# # This file is licensed under the Affero General Public License (AGPL) version 3. # # Copyright 2020 The Matrix.org Foundation C.I.C. # Copyright (C) 2023 New Vector, Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # See the GNU Affero General Public License for more details: # . # # Originally licensed under the Apache License, Version 2.0: # . # # [This file includes modifications made by New Vector Limited] # # import logging from typing import Dict, List, Optional, Tuple, cast import attr from parameterized import parameterized from twisted.test.proto_helpers import MemoryReactor from synapse.api.constants import EventContentFields, EventTypes, Membership, RoomTypes from synapse.api.room_versions import RoomVersions from synapse.events import EventBase, StrippedStateEvent, make_event_from_dict from synapse.events.snapshot import EventContext from synapse.federation.federation_base import event_from_pdu_json from synapse.rest import admin from synapse.rest.client import login, room from synapse.server import HomeServer from synapse.storage.databases.main.events import DeltaState from synapse.storage.databases.main.events_bg_updates import _BackgroundUpdates from synapse.types import StateMap from synapse.util import Clock from tests.test_utils.event_injection import create_event from tests.unittest import HomeserverTestCase logger = logging.getLogger(__name__) class ExtremPruneTestCase(HomeserverTestCase): servlets = [ admin.register_servlets, room.register_servlets, login.register_servlets, ] def prepare( self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer ) -> None: self.state = self.hs.get_state_handler() persistence = self.hs.get_storage_controllers().persistence assert persistence is not None self._persistence = persistence self._state_storage_controller = self.hs.get_storage_controllers().state self.store = self.hs.get_datastores().main self.register_user("user", "pass") self.token = self.login("user", "pass") self.room_id = self.helper.create_room_as( "user", room_version=RoomVersions.V6.identifier, tok=self.token ) body = self.helper.send(self.room_id, body="Test", tok=self.token) local_message_event_id = body["event_id"] # Fudge a remote event and persist it. This will be the extremity before # the gap. self.remote_event_1 = event_from_pdu_json( { "type": EventTypes.Message, "state_key": "@user:other", "content": {}, "room_id": self.room_id, "sender": "@user:other", "depth": 5, "prev_events": [local_message_event_id], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) self.persist_event(self.remote_event_1) # Check that the current extremities is the remote event. self.assert_extremities([self.remote_event_1.event_id]) def persist_event( self, event: EventBase, state: Optional[StateMap[str]] = None ) -> None: """Persist the event, with optional state""" context = self.get_success( self.state.compute_event_context( event, state_ids_before_event=state, partial_state=None if state is None else False, ) ) self.get_success(self._persistence.persist_event(event, context)) def assert_extremities(self, expected_extremities: List[str]) -> None: """Assert the current extremities for the room""" extremities = self.get_success( self.store.get_prev_events_for_room(self.room_id) ) self.assertCountEqual(extremities, expected_extremities) def test_prune_gap(self) -> None: """Test that we drop extremities after a gap when we see an event from the same domain. """ # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other", "depth": 50, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([remote_event_2.event_id]) def test_do_not_prune_gap_if_state_different(self) -> None: """Test that we don't prune extremities after a gap if the resolved state is different. """ # Fudge a second event which points to an event we don't have. remote_event_2 = event_from_pdu_json( { "type": EventTypes.Message, "state_key": "@user:other", "content": {}, "room_id": self.room_id, "sender": "@user:other", "depth": 10, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) # Now we persist it with state with a dropped history visibility # setting. The state resolution across the old and new event will then # include it, and so the resolved state won't match the new state. state_before_gap = dict( self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) ) state_before_gap.pop(("m.room.history_visibility", "")) context = self.get_success( self.state.compute_event_context( remote_event_2, state_ids_before_event=state_before_gap, partial_state=False, ) ) self.get_success(self._persistence.persist_event(remote_event_2, context)) # Check that we haven't dropped the old extremity. self.assert_extremities([self.remote_event_1.event_id, remote_event_2.event_id]) def test_prune_gap_if_old(self) -> None: """Test that we drop extremities after a gap when the previous extremity is "old" """ # Advance the clock for many days to make the old extremity "old". We # also set the depth to "lots". self.reactor.advance(7 * 24 * 60 * 60) # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other2", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other2", "depth": 10000, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([remote_event_2.event_id]) def test_do_not_prune_gap_if_other_server(self) -> None: """Test that we do not drop extremities after a gap when we see an event from a different domain. """ # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other2", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other2", "depth": 10, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([self.remote_event_1.event_id, remote_event_2.event_id]) def test_prune_gap_if_dummy_remote(self) -> None: """Test that we drop extremities after a gap when the previous extremity is a local dummy event and only points to remote events. """ body = self.helper.send_event( self.room_id, type=EventTypes.Dummy, content={}, tok=self.token ) local_message_event_id = body["event_id"] self.assert_extremities([local_message_event_id]) # Advance the clock for many days to make the old extremity "old". We # also set the depth to "lots". self.reactor.advance(7 * 24 * 60 * 60) # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other2", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other2", "depth": 10000, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([remote_event_2.event_id]) def test_prune_gap_if_dummy_local(self) -> None: """Test that we don't drop extremities after a gap when the previous extremity is a local dummy event and points to local events. """ body = self.helper.send(self.room_id, body="Test", tok=self.token) body = self.helper.send_event( self.room_id, type=EventTypes.Dummy, content={}, tok=self.token ) local_message_event_id = body["event_id"] self.assert_extremities([local_message_event_id]) # Advance the clock for many days to make the old extremity "old". We # also set the depth to "lots". self.reactor.advance(7 * 24 * 60 * 60) # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other2", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other2", "depth": 10000, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([remote_event_2.event_id, local_message_event_id]) def test_do_not_prune_gap_if_not_dummy(self) -> None: """Test that we do not drop extremities after a gap when the previous extremity is not a dummy event. """ body = self.helper.send(self.room_id, body="test", tok=self.token) local_message_event_id = body["event_id"] self.assert_extremities([local_message_event_id]) # Fudge a second event which points to an event we don't have. This is a # state event so that the state changes (otherwise we won't prune the # extremity as they'll have the same state group). remote_event_2 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": "@user:other2", "content": {"membership": Membership.JOIN}, "room_id": self.room_id, "sender": "@user:other2", "depth": 10000, "prev_events": ["$some_unknown_message"], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) state_before_gap = self.get_success( self._state_storage_controller.get_current_state_ids(self.room_id) ) self.persist_event(remote_event_2, state=state_before_gap) # Check the new extremity is just the new remote event. self.assert_extremities([local_message_event_id, remote_event_2.event_id]) class InvalideUsersInRoomCacheTestCase(HomeserverTestCase): servlets = [ admin.register_servlets, room.register_servlets, login.register_servlets, ] def prepare( self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer ) -> None: self.state = self.hs.get_state_handler() persistence = self.hs.get_storage_controllers().persistence assert persistence is not None self._persistence = persistence self.store = self.hs.get_datastores().main def test_remote_user_rooms_cache_invalidated(self) -> None: """Test that if the server leaves a room the `get_rooms_for_user` cache is invalidated for remote users. """ # Set up a room with a local and remote user in it. user_id = self.register_user("user", "pass") token = self.login("user", "pass") room_id = self.helper.create_room_as( "user", room_version=RoomVersions.V6.identifier, tok=token ) body = self.helper.send(room_id, body="Test", tok=token) local_message_event_id = body["event_id"] # Fudge a join event for a remote user. remote_user = "@user:other" remote_event_1 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": remote_user, "content": {"membership": Membership.JOIN}, "room_id": room_id, "sender": remote_user, "depth": 5, "prev_events": [local_message_event_id], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) context = self.get_success(self.state.compute_event_context(remote_event_1)) self.get_success(self._persistence.persist_event(remote_event_1, context)) # Call `get_rooms_for_user` to add the remote user to the cache rooms = self.get_success(self.store.get_rooms_for_user(remote_user)) self.assertEqual(set(rooms), {room_id}) # Now we have the local server leave the room, and check that calling # `get_user_in_room` for the remote user no longer includes the room. self.helper.leave(room_id, user_id, tok=token) rooms = self.get_success(self.store.get_rooms_for_user(remote_user)) self.assertEqual(set(rooms), set()) def test_room_remote_user_cache_invalidated(self) -> None: """Test that if the server leaves a room the `get_users_in_room` cache is invalidated for remote users. """ # Set up a room with a local and remote user in it. user_id = self.register_user("user", "pass") token = self.login("user", "pass") room_id = self.helper.create_room_as( "user", room_version=RoomVersions.V6.identifier, tok=token ) body = self.helper.send(room_id, body="Test", tok=token) local_message_event_id = body["event_id"] # Fudge a join event for a remote user. remote_user = "@user:other" remote_event_1 = event_from_pdu_json( { "type": EventTypes.Member, "state_key": remote_user, "content": {"membership": Membership.JOIN}, "room_id": room_id, "sender": remote_user, "depth": 5, "prev_events": [local_message_event_id], "auth_events": [], "origin_server_ts": self.clock.time_msec(), }, RoomVersions.V6, ) context = self.get_success(self.state.compute_event_context(remote_event_1)) self.get_success(self._persistence.persist_event(remote_event_1, context)) # Call `get_users_in_room` to add the remote user to the cache users = self.get_success(self.store.get_users_in_room(room_id)) self.assertEqual(set(users), {user_id, remote_user}) # Now we have the local server leave the room, and check that calling # `get_user_in_room` for the remote user no longer includes the room. self.helper.leave(room_id, user_id, tok=token) users = self.get_success(self.store.get_users_in_room(room_id)) self.assertEqual(users, []) @attr.s(slots=True, frozen=True, auto_attribs=True) class _SlidingSyncJoinedRoomResult: room_id: str # `event_stream_ordering` is only optional to allow easier semantics when we make # expected objects from `event.internal_metadata.stream_ordering`. in the tests. # `event.internal_metadata.stream_ordering` is marked optional because it only # exists for persisted events but in the context of these tests, we're only working # with persisted events and we're making comparisons so we will find any mismatch. event_stream_ordering: Optional[int] bump_stamp: Optional[int] room_type: Optional[str] room_name: Optional[str] is_encrypted: bool tombstone_successor_room_id: Optional[str] @attr.s(slots=True, frozen=True, auto_attribs=True) class _SlidingSyncMembershipSnapshotResult: room_id: str user_id: str sender: str membership_event_id: str membership: str # `event_stream_ordering` is only optional to allow easier semantics when we make # expected objects from `event.internal_metadata.stream_ordering`. in the tests. # `event.internal_metadata.stream_ordering` is marked optional because it only # exists for persisted events but in the context of these tests, we're only working # with persisted events and we're making comparisons so we will find any mismatch. event_stream_ordering: Optional[int] has_known_state: bool room_type: Optional[str] room_name: Optional[str] is_encrypted: bool tombstone_successor_room_id: Optional[str] class SlidingSyncPrePopulatedTablesTestCase(HomeserverTestCase): """ Tests to make sure the `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots` database tables are populated correctly. """ servlets = [ admin.register_servlets, login.register_servlets, room.register_servlets, ] def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.store = hs.get_datastores().main self.storage_controllers = hs.get_storage_controllers() persist_events_store = self.hs.get_datastores().persist_events assert persist_events_store is not None self.persist_events_store = persist_events_store def _get_sliding_sync_joined_rooms(self) -> Dict[str, _SlidingSyncJoinedRoomResult]: """ Return the rows from the `sliding_sync_joined_rooms` table. Returns: Mapping from room_id to _SlidingSyncJoinedRoomResult. """ rows = cast( List[Tuple[str, int, int, str, str, bool, str]], self.get_success( self.store.db_pool.simple_select_list( "sliding_sync_joined_rooms", None, retcols=( "room_id", "event_stream_ordering", "bump_stamp", "room_type", "room_name", "is_encrypted", "tombstone_successor_room_id", ), ), ), ) return { row[0]: _SlidingSyncJoinedRoomResult( room_id=row[0], event_stream_ordering=row[1], bump_stamp=row[2], room_type=row[3], room_name=row[4], is_encrypted=bool(row[5]), tombstone_successor_room_id=row[6], ) for row in rows } def _get_sliding_sync_membership_snapshots( self, ) -> Dict[Tuple[str, str], _SlidingSyncMembershipSnapshotResult]: """ Return the rows from the `sliding_sync_membership_snapshots` table. Returns: Mapping from the (room_id, user_id) to _SlidingSyncMembershipSnapshotResult. """ rows = cast( List[Tuple[str, str, str, str, str, int, bool, str, str, bool, str]], self.get_success( self.store.db_pool.simple_select_list( "sliding_sync_membership_snapshots", None, retcols=( "room_id", "user_id", "sender", "membership_event_id", "membership", "event_stream_ordering", "has_known_state", "room_type", "room_name", "is_encrypted", "tombstone_successor_room_id", ), ), ), ) return { (row[0], row[1]): _SlidingSyncMembershipSnapshotResult( room_id=row[0], user_id=row[1], sender=row[2], membership_event_id=row[3], membership=row[4], event_stream_ordering=row[5], has_known_state=bool(row[6]), room_type=row[7], room_name=row[8], is_encrypted=bool(row[9]), tombstone_successor_room_id=row[10], ) for row in rows } _remote_invite_count: int = 0 def _create_remote_invite_room_for_user( self, invitee_user_id: str, unsigned_invite_room_state: Optional[List[StrippedStateEvent]], ) -> Tuple[str, EventBase]: """ Create a fake invite for a remote room and persist it. We don't have any state for these kind of rooms and can only rely on the stripped state included in the unsigned portion of the invite event to identify the room. Args: invitee_user_id: The person being invited unsigned_invite_room_state: List of stripped state events to assist the receiver in identifying the room. Returns: The room ID of the remote invite room and the persisted remote invite event. """ invite_room_id = f"!test_room{self._remote_invite_count}:remote_server" invite_event_dict = { "room_id": invite_room_id, "sender": "@inviter:remote_server", "state_key": invitee_user_id, "depth": 1, "origin_server_ts": 1, "type": EventTypes.Member, "content": {"membership": Membership.INVITE}, "auth_events": [], "prev_events": [], } if unsigned_invite_room_state is not None: serialized_stripped_state_events = [] for stripped_event in unsigned_invite_room_state: serialized_stripped_state_events.append( { "type": stripped_event.type, "state_key": stripped_event.state_key, "sender": stripped_event.sender, "content": stripped_event.content, } ) invite_event_dict["unsigned"] = { "invite_room_state": serialized_stripped_state_events } invite_event = make_event_from_dict( invite_event_dict, room_version=RoomVersions.V10, ) invite_event.internal_metadata.outlier = True invite_event.internal_metadata.out_of_band_membership = True self.get_success( self.store.maybe_store_room_on_outlier_membership( room_id=invite_room_id, room_version=invite_event.room_version ) ) context = EventContext.for_outlier(self.hs.get_storage_controllers()) persist_controller = self.hs.get_storage_controllers().persistence assert persist_controller is not None persisted_event, _, _ = self.get_success( persist_controller.persist_event(invite_event, context) ) self._remote_invite_count += 1 return invite_room_id, persisted_event def _retract_remote_invite_for_user( self, user_id: str, remote_room_id: str, ) -> EventBase: """ Create a fake invite retraction for a remote room and persist it. Retracting an invite just means the person is no longer invited to the room. This is done by someone with proper power levels kicking the user from the room. A kick shows up as a leave event for a given person with a different `sender`. Args: user_id: The person who was invited and we're going to retract the invite for. remote_room_id: The room ID that the invite was for. Returns: The persisted leave (kick) event. """ kick_event_dict = { "room_id": remote_room_id, "sender": "@inviter:remote_server", "state_key": user_id, "depth": 1, "origin_server_ts": 1, "type": EventTypes.Member, "content": {"membership": Membership.LEAVE}, "auth_events": [], "prev_events": [], } kick_event = make_event_from_dict( kick_event_dict, room_version=RoomVersions.V10, ) kick_event.internal_metadata.outlier = True kick_event.internal_metadata.out_of_band_membership = True self.get_success( self.store.maybe_store_room_on_outlier_membership( room_id=remote_room_id, room_version=kick_event.room_version ) ) context = EventContext.for_outlier(self.hs.get_storage_controllers()) persist_controller = self.hs.get_storage_controllers().persistence assert persist_controller is not None persisted_event, _, _ = self.get_success( persist_controller.persist_event(kick_event, context) ) return persisted_event def test_joined_room_with_no_info(self) -> None: """ Test joined room that doesn't have a room type, encryption, or name shows up in `sliding_sync_joined_rooms`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # History visibility just happens to be the last event sent in the room event_stream_ordering=state_map[ (EventTypes.RoomHistoryVisibility, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), }, exact=True, ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_joined_room_with_info(self) -> None: """ Test joined encrypted room with name shows up in `sliding_sync_joined_rooms`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # Encrypt the room self.helper.send_state( room_id1, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user2_tok, ) # Add a tombstone self.helper.send_state( room_id1, EventTypes.Tombstone, {EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room"}, tok=user2_tok, ) # User1 joins the room self.helper.join(room_id1, user1_id, tok=user1_tok) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # This should be whatever is the last event in the room event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, # Even though this room does have a name, is encrypted, and has a # tombstone, user2 is the room creator and joined at the room creation # time which didn't have this state set yet. room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_joined_space_room_with_info(self) -> None: """ Test joined space room with name shows up in `sliding_sync_joined_rooms`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") space_room_id = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user2_tok, ) # User1 joins the room user1_join_response = self.helper.join(space_room_id, user1_id, tok=user1_tok) user1_join_event_pos = self.get_success( self.store.get_position_for_event(user1_join_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(space_room_id) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {space_room_id}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[space_room_id], _SlidingSyncJoinedRoomResult( room_id=space_room_id, event_stream_ordering=user1_join_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (space_room_id, user1_id), (space_room_id, user2_id), }, exact=True, ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, # Even though this room does have a name, user2 is the room creator and # joined at the room creation time which didn't have this state set yet. room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_joined_room_with_state_updated(self) -> None: """ Test state derived info in `sliding_sync_joined_rooms` is updated when the current state is updated. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # User1 joins the room user1_join_response = self.helper.join(room_id1, user1_id, tok=user1_tok) user1_join_event_pos = self.get_success( self.store.get_position_for_event(user1_join_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, event_stream_ordering=user1_join_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Update the room name self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room was renamed"}, tok=user2_tok, ) # Encrypt the room encrypt_room_response = self.helper.send_state( room_id1, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user2_tok, ) encrypt_room_event_pos = self.get_success( self.store.get_position_for_event(encrypt_room_response["event_id"]) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) # Make sure we see the new room name self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, event_stream_ordering=encrypt_room_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room was renamed", is_encrypted=True, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_joined_room_is_bumped(self) -> None: """ Test that `event_stream_ordering` and `bump_stamp` is updated when a new bump event is sent (`sliding_sync_joined_rooms`). """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # User1 joins the room user1_join_response = self.helper.join(room_id1, user1_id, tok=user1_tok) user1_join_event_pos = self.get_success( self.store.get_position_for_event(user1_join_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, event_stream_ordering=user1_join_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user joined user1_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), user1_snapshot, ) # Holds the info according to the current state when the user joined user2_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), user2_snapshot, ) # Send a new message to bump the room event_response = self.helper.send(room_id1, "some message", tok=user1_tok) event_pos = self.get_success( self.store.get_position_for_event(event_response["event_id"]) ) sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) # Make sure we see the new room name self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # Updated `event_stream_ordering` event_stream_ordering=event_pos.stream, # And since the event was a bump event, the `bump_stamp` should be updated bump_stamp=event_pos.stream, # The state is still the same (it didn't change) room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), user1_snapshot, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), user2_snapshot, ) def test_joined_room_meta_state_reset(self) -> None: """ Test that a state reset on the room name is reflected in the `sliding_sync_joined_rooms` table. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # User1 joins the room self.helper.join(room_id, user1_id, tok=user1_tok) # Make sure we see the new room name sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id}, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id], _SlidingSyncJoinedRoomResult( room_id=room_id, # This should be whatever is the last event in the room event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id, user1_id), (room_id, user2_id), }, exact=True, ) user1_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user1_id)), user1_snapshot, ) # Holds the info according to the current state when the user joined (no room # name when the room creator joined) user2_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user2_id)), user2_snapshot, ) # Mock a state reset removing the room name state from the current state message_tuple = self.get_success( create_event( self.hs, prev_event_ids=[state_map[(EventTypes.Name, "")].event_id], auth_event_ids=[ state_map[(EventTypes.Create, "")].event_id, state_map[(EventTypes.Member, user1_id)].event_id, ], type=EventTypes.Message, content={"body": "foo", "msgtype": "m.text"}, sender=user1_id, room_id=room_id, room_version=RoomVersions.V10.identifier, ) ) event_chunk = [message_tuple] self.get_success( self.persist_events_store._persist_events_and_state_updates( room_id, event_chunk, state_delta_for_room=DeltaState( # This is the state reset part. We're removing the room name state. to_delete=[(EventTypes.Name, "")], to_insert={}, ), new_forward_extremities={message_tuple[0].event_id}, use_negative_stream_ordering=False, inhibit_local_membership_updates=False, new_event_links={}, ) ) # Make sure the state reset is reflected in the `sliding_sync_joined_rooms` table sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id}, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id], _SlidingSyncJoinedRoomResult( room_id=room_id, # This should be whatever is the last event in the room event_stream_ordering=message_tuple[ 0 ].internal_metadata.stream_ordering, bump_stamp=message_tuple[0].internal_metadata.stream_ordering, room_type=None, # This was state reset back to None room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) # State reset shouldn't be reflected in the `sliding_sync_membership_snapshots` sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id, user1_id), (room_id, user2_id), }, exact=True, ) # Snapshots haven't changed self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user1_id)), user1_snapshot, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user2_id)), user2_snapshot, ) def test_non_join_space_room_with_info(self) -> None: """ Test users who was invited shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") space_room_id = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user2_tok, ) # Encrypt the room self.helper.send_state( space_room_id, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user2_tok, ) # Add a tombstone self.helper.send_state( space_room_id, EventTypes.Tombstone, {EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room"}, tok=user2_tok, ) # User1 is invited to the room user1_invited_response = self.helper.invite( space_room_id, src=user2_id, targ=user1_id, tok=user2_tok ) user1_invited_event_pos = self.get_success( self.store.get_position_for_event(user1_invited_response["event_id"]) ) # Update the room name after we are invited just to make sure # we don't update non-join memberships when the room name changes. rename_response = self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space was renamed"}, tok=user2_tok, ) rename_event_pos = self.get_success( self.store.get_position_for_event(rename_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(space_room_id) ) # User2 is still joined to the room so we should still have an entry in the # `sliding_sync_joined_rooms` table. sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {space_room_id}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[space_room_id], _SlidingSyncJoinedRoomResult( room_id=space_room_id, event_stream_ordering=rename_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=RoomTypes.SPACE, room_name="my super duper space was renamed", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (space_room_id, user1_id), (space_room_id, user2_id), }, exact=True, ) # Holds the info according to the current state when the user was invited self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender=user2_id, membership_event_id=user1_invited_response["event_id"], membership=Membership.INVITE, event_stream_ordering=user1_invited_event_pos.stream, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_non_join_invite_ban(self) -> None: """ Test users who have invite/ban membership in room shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") user3_tok = self.login(user3_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # User1 is invited to the room user1_invited_response = self.helper.invite( room_id1, src=user2_id, targ=user1_id, tok=user2_tok ) user1_invited_event_pos = self.get_success( self.store.get_position_for_event(user1_invited_response["event_id"]) ) # User3 joins the room self.helper.join(room_id1, user3_id, tok=user3_tok) # User3 is banned from the room user3_ban_response = self.helper.ban( room_id1, src=user2_id, targ=user3_id, tok=user2_tok ) user3_ban_event_pos = self.get_success( self.store.get_position_for_event(user3_ban_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) # User2 is still joined to the room so we should still have an entry # in the `sliding_sync_joined_rooms` table. sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, event_stream_ordering=user3_ban_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), (room_id1, user3_id), }, exact=True, ) # Holds the info according to the current state when the user was invited self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user2_id, membership_event_id=user1_invited_response["event_id"], membership=Membership.INVITE, event_stream_ordering=user1_invited_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user was banned self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user3_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user3_id, sender=user2_id, membership_event_id=user3_ban_response["event_id"], membership=Membership.BAN, event_stream_ordering=user3_ban_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_non_join_reject_invite_empty_room(self) -> None: """ In a room where no one is joined (`no_longer_in_room`), test rejecting an invite. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # User1 is invited to the room self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) # User2 leaves the room user2_leave_response = self.helper.leave(room_id1, user2_id, tok=user2_tok) user2_leave_event_pos = self.get_success( self.store.get_position_for_event(user2_leave_response["event_id"]) ) # User1 rejects the invite user1_leave_response = self.helper.leave(room_id1, user1_id, tok=user1_tok) user1_leave_event_pos = self.get_success( self.store.get_position_for_event(user1_leave_response["event_id"]) ) # No one is joined to the room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user left self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=user1_leave_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=user1_leave_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the left self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=user2_leave_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=user2_leave_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_membership_changing(self) -> None: """ Test latest snapshot evolves when membership changes (`sliding_sync_membership_snapshots`). """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # User1 is invited to the room # ====================================================== user1_invited_response = self.helper.invite( room_id1, src=user2_id, targ=user1_id, tok=user2_tok ) user1_invited_event_pos = self.get_success( self.store.get_position_for_event(user1_invited_response["event_id"]) ) # Update the room name after the user was invited room_name_update_response = self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) room_name_update_event_pos = self.get_success( self.store.get_position_for_event(room_name_update_response["event_id"]) ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id1) ) # Assert joined room status sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # Latest event in the room event_stream_ordering=room_name_update_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Assert membership snapshots sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user was invited self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user2_id, membership_event_id=user1_invited_response["event_id"], membership=Membership.INVITE, event_stream_ordering=user1_invited_event_pos.stream, has_known_state=True, room_type=None, # Room name was updated after the user was invited so we should still # see it unset here room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined user2_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), user2_snapshot, ) # User1 joins the room # ====================================================== user1_joined_response = self.helper.join(room_id1, user1_id, tok=user1_tok) user1_joined_event_pos = self.get_success( self.store.get_position_for_event(user1_joined_response["event_id"]) ) # Assert joined room status sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # Latest event in the room event_stream_ordering=user1_joined_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Assert membership snapshots sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=user1_joined_response["event_id"], membership=Membership.JOIN, event_stream_ordering=user1_joined_event_pos.stream, has_known_state=True, room_type=None, # We see the update state because the user joined after the room name # change room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), user2_snapshot, ) # User1 is banned from the room # ====================================================== user1_ban_response = self.helper.ban( room_id1, src=user2_id, targ=user1_id, tok=user2_tok ) user1_ban_event_pos = self.get_success( self.store.get_position_for_event(user1_ban_response["event_id"]) ) # Assert joined room status sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id1}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id1], _SlidingSyncJoinedRoomResult( room_id=room_id1, # Latest event in the room event_stream_ordering=user1_ban_event_pos.stream, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Assert membership snapshots sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) # Holds the info according to the current state when the user was banned self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user2_id, membership_event_id=user1_ban_response["event_id"], membership=Membership.BAN, event_stream_ordering=user1_ban_event_pos.stream, has_known_state=True, room_type=None, # We see the update state because the user joined after the room name # change room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Holds the info according to the current state when the user joined self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), user2_snapshot, ) def test_non_join_server_left_room(self) -> None: """ Test everyone local leaves the room but their leave membership still shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # User1 joins the room self.helper.join(room_id1, user1_id, tok=user1_tok) # User2 leaves the room user2_leave_response = self.helper.leave(room_id1, user2_id, tok=user2_tok) user2_leave_event_pos = self.get_success( self.store.get_position_for_event(user2_leave_response["event_id"]) ) # User1 leaves the room user1_leave_response = self.helper.leave(room_id1, user1_id, tok=user1_tok) user1_leave_event_pos = self.get_success( self.store.get_position_for_event(user1_leave_response["event_id"]) ) # No one is joined to the room anymore so we shouldn't have an entry in the # `sliding_sync_joined_rooms` table. sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) # We should still see rows for the leave events (non-joins) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id1, user1_id), (room_id1, user2_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user1_id, sender=user1_id, membership_event_id=user1_leave_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=user1_leave_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id1, user2_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id1, user_id=user2_id, sender=user2_id, membership_event_id=user2_leave_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=user2_leave_event_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) @parameterized.expand( [ # No stripped state provided ("none", None), # Empty stripped state provided ("empty", []), ] ) def test_non_join_remote_invite_no_stripped_state( self, _description: str, stripped_state: Optional[List[StrippedStateEvent]] ) -> None: """ Test remote invite with no stripped state provided shows up in `sliding_sync_membership_snapshots` with `has_known_state=False`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create a remote invite room without any `unsigned.invite_room_state` remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user(user1_id, stripped_state) ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=remote_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=remote_invite_event.internal_metadata.stream_ordering, # No stripped state provided has_known_state=False, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_non_join_remote_invite_unencrypted_room(self) -> None: """ Test remote invite with stripped state (unencrypted room) shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create a remote invite room with some `unsigned.invite_room_state` # indicating that the room is encrypted. remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper room", }, ), ], ) ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=remote_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=remote_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_non_join_remote_invite_encrypted_room(self) -> None: """ Test remote invite with stripped state (encrypted room) shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create a remote invite room with some `unsigned.invite_room_state` # indicating that the room is encrypted. remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), # This is not one of the stripped state events according to the state # but we still handle it. StrippedStateEvent( type=EventTypes.Tombstone, state_key="", sender="@inviter:remote_server", content={ EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room", }, ), # Also test a random event that we don't care about StrippedStateEvent( type="org.matrix.foo_state", state_key="", sender="@inviter:remote_server", content={ "foo": "qux", }, ), ], ) ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=remote_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=remote_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=True, tombstone_successor_room_id="another_room", ), ) def test_non_join_remote_invite_space_room(self) -> None: """ Test remote invite with stripped state (encrypted space room with name) shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create a remote invite room with some `unsigned.invite_room_state` # indicating that the room is encrypted. remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, # Specify that it is a space room EventContentFields.ROOM_TYPE: RoomTypes.SPACE, }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper space", }, ), ], ) ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=remote_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=remote_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=True, tombstone_successor_room_id=None, ), ) def test_non_join_reject_remote_invite(self) -> None: """ Test rejected remote invite (user decided to leave the room) inherits meta data from when the remote invite stripped state and shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create a remote invite room with some `unsigned.invite_room_state` # indicating that the room is encrypted. remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), ], ) ) # User1 decides to leave the room (reject the invite) user1_leave_response = self.helper.leave( remote_invite_room_id, user1_id, tok=user1_tok ) user1_leave_pos = self.get_success( self.store.get_position_for_event(user1_leave_response["event_id"]) ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender=user1_id, membership_event_id=user1_leave_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=user1_leave_pos.stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=True, tombstone_successor_room_id=None, ), ) def test_non_join_retracted_remote_invite(self) -> None: """ Test retracted remote invite (Remote inviter kicks the person who was invited) inherits meta data from when the remote invite stripped state and shows up in `sliding_sync_membership_snapshots`. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create a remote invite room with some `unsigned.invite_room_state` # indicating that the room is encrypted. remote_invite_room_id, remote_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), ], ) ) # `@inviter:remote_server` decides to retract the invite (kicks the user). # (Note: A kick is just a leave event with a different sender) remote_invite_retraction_event = self._retract_remote_invite_for_user( user_id=user1_id, remote_room_id=remote_invite_room_id, ) # No one local is joined to the remote room sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (remote_invite_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (remote_invite_room_id, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=remote_invite_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=remote_invite_retraction_event.event_id, membership=Membership.LEAVE, event_stream_ordering=remote_invite_retraction_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=True, tombstone_successor_room_id=None, ), ) def test_non_join_state_reset(self) -> None: """ Test a state reset that removes someone from the room. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # User1 joins the room self.helper.join(room_id, user1_id, tok=user1_tok) # Make sure we see the new room name sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id}, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id], _SlidingSyncJoinedRoomResult( room_id=room_id, # This should be whatever is the last event in the room event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id, user1_id), (room_id, user2_id), }, exact=True, ) user1_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user1_id)), user1_snapshot, ) # Holds the info according to the current state when the user joined (no room # name when the room creator joined) user2_snapshot = _SlidingSyncMembershipSnapshotResult( room_id=room_id, user_id=user2_id, sender=user2_id, membership_event_id=state_map[(EventTypes.Member, user2_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user2_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user2_id)), user2_snapshot, ) # Mock a state reset removing the membership for user1 in the current state message_tuple = self.get_success( create_event( self.hs, prev_event_ids=[state_map[(EventTypes.Name, "")].event_id], auth_event_ids=[ state_map[(EventTypes.Create, "")].event_id, state_map[(EventTypes.Member, user1_id)].event_id, ], type=EventTypes.Message, content={"body": "foo", "msgtype": "m.text"}, sender=user1_id, room_id=room_id, room_version=RoomVersions.V10.identifier, ) ) event_chunk = [message_tuple] self.get_success( self.persist_events_store._persist_events_and_state_updates( room_id, event_chunk, state_delta_for_room=DeltaState( # This is the state reset part. We're removing the room name state. to_delete=[(EventTypes.Member, user1_id)], to_insert={}, ), new_forward_extremities={message_tuple[0].event_id}, use_negative_stream_ordering=False, inhibit_local_membership_updates=False, new_event_links={}, ) ) # State reset on membership doesn't affect the`sliding_sync_joined_rooms` table sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id}, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id], _SlidingSyncJoinedRoomResult( room_id=room_id, # This should be whatever is the last event in the room event_stream_ordering=message_tuple[ 0 ].internal_metadata.stream_ordering, bump_stamp=message_tuple[0].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # State reset on membership should remove the user's snapshot sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { # We shouldn't see user1 in the snapshots table anymore (room_id, user2_id), }, exact=True, ) # Snapshot for user2 hasn't changed self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id, user2_id)), user2_snapshot, ) def test_joined_background_update_missing(self) -> None: """ Test that the background update for `sliding_sync_joined_rooms` backfills missing rows """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_no_info = self.helper.create_room_as(user1_id, tok=user1_tok) room_id_with_info = self.helper.create_room_as(user1_id, tok=user1_tok) # Add a room name self.helper.send_state( room_id_with_info, EventTypes.Name, {"name": "my super duper room"}, tok=user1_tok, ) # Encrypt the room self.helper.send_state( room_id_with_info, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user1_tok, ) space_room_id = self.helper.create_room_as( user1_id, tok=user1_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user1_tok, ) # Clean-up the `sliding_sync_joined_rooms` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_joined_rooms", column="room_id", iterable=(room_id_no_info, room_id_with_info, space_room_id), keyvalues={}, desc="sliding_sync_joined_rooms.test_joined_background_update_missing", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_JOINED_ROOMS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id_no_info, room_id_with_info, space_room_id}, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id_no_info) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id_no_info], _SlidingSyncJoinedRoomResult( room_id=room_id_no_info, # History visibility just happens to be the last event sent in the room event_stream_ordering=state_map[ (EventTypes.RoomHistoryVisibility, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id_with_info) ) self.assertEqual( sliding_sync_joined_rooms_results[room_id_with_info], _SlidingSyncJoinedRoomResult( room_id=room_id_with_info, # Lastest event sent in the room event_stream_ordering=state_map[ (EventTypes.RoomEncryption, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id=None, ), ) state_map = self.get_success( self.storage_controllers.state.get_current_state(space_room_id) ) self.assertEqual( sliding_sync_joined_rooms_results[space_room_id], _SlidingSyncJoinedRoomResult( room_id=space_room_id, # Lastest event sent in the room event_stream_ordering=state_map[ (EventTypes.Name, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_joined_background_update_partial(self) -> None: """ Test that the background update for `sliding_sync_joined_rooms` backfills partially updated rows. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_with_info = self.helper.create_room_as(user1_id, tok=user1_tok) # Add a room name self.helper.send_state( room_id_with_info, EventTypes.Name, {"name": "my super duper room"}, tok=user1_tok, ) # Encrypt the room self.helper.send_state( room_id_with_info, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user1_tok, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id_with_info) ) # Clean-up the `sliding_sync_joined_rooms` table as if the the encryption event # never made it into the table. self.get_success( self.store.db_pool.simple_update( table="sliding_sync_joined_rooms", keyvalues={"room_id": room_id_with_info}, updatevalues={"is_encrypted": False}, desc="sliding_sync_joined_rooms.test_joined_background_update_partial", ) ) # We should see the partial row that we made in preparation for the test. sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id_with_info}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id_with_info], _SlidingSyncJoinedRoomResult( room_id=room_id_with_info, # Lastest event sent in the room event_stream_ordering=state_map[ (EventTypes.RoomEncryption, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=False, tombstone_successor_room_id=None, ), ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_JOINED_ROOMS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_joined_rooms_results = self._get_sliding_sync_joined_rooms() self.assertIncludes( set(sliding_sync_joined_rooms_results.keys()), {room_id_with_info}, exact=True, ) self.assertEqual( sliding_sync_joined_rooms_results[room_id_with_info], _SlidingSyncJoinedRoomResult( room_id=room_id_with_info, # Lastest event sent in the room event_stream_ordering=state_map[ (EventTypes.RoomEncryption, "") ].internal_metadata.stream_ordering, bump_stamp=state_map[ (EventTypes.Create, "") ].internal_metadata.stream_ordering, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id=None, ), ) def test_membership_snapshots_background_update_joined(self) -> None: """ Test that the background update for `sliding_sync_membership_snapshots` backfills missing rows for join memberships. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_no_info = self.helper.create_room_as(user1_id, tok=user1_tok) room_id_with_info = self.helper.create_room_as(user1_id, tok=user1_tok) # Add a room name self.helper.send_state( room_id_with_info, EventTypes.Name, {"name": "my super duper room"}, tok=user1_tok, ) # Encrypt the room self.helper.send_state( room_id_with_info, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user1_tok, ) # Add a tombstone self.helper.send_state( room_id_with_info, EventTypes.Tombstone, {EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room"}, tok=user1_tok, ) space_room_id = self.helper.create_room_as( user1_id, tok=user1_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user1_tok, ) # Clean-up the `sliding_sync_membership_snapshots` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_membership_snapshots", column="room_id", iterable=(room_id_no_info, room_id_with_info, space_room_id), keyvalues={}, desc="sliding_sync_membership_snapshots.test_membership_snapshots_background_update_joined", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { (room_id_no_info, user1_id), (room_id_with_info, user1_id), (space_room_id, user1_id), }, exact=True, ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id_no_info) ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id_no_info, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id_no_info, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) state_map = self.get_success( self.storage_controllers.state.get_current_state(room_id_with_info) ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_with_info, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_with_info, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) state_map = self.get_success( self.storage_controllers.state.get_current_state(space_room_id) ) self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender=user1_id, membership_event_id=state_map[(EventTypes.Member, user1_id)].event_id, membership=Membership.JOIN, event_stream_ordering=state_map[ (EventTypes.Member, user1_id) ].internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_membership_snapshots_background_update_local_invite(self) -> None: """ Test that the background update for `sliding_sync_membership_snapshots` backfills missing rows for invite memberships. """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_no_info = self.helper.create_room_as(user2_id, tok=user2_tok) room_id_with_info = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id_with_info, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # Encrypt the room self.helper.send_state( room_id_with_info, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user2_tok, ) # Add a tombstone self.helper.send_state( room_id_with_info, EventTypes.Tombstone, {EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room"}, tok=user2_tok, ) space_room_id = self.helper.create_room_as( user1_id, tok=user2_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user2_tok, ) # Invite user1 to the rooms user1_invite_room_id_no_info_response = self.helper.invite( room_id_no_info, src=user2_id, targ=user1_id, tok=user2_tok ) user1_invite_room_id_with_info_response = self.helper.invite( room_id_with_info, src=user2_id, targ=user1_id, tok=user2_tok ) user1_invite_space_room_id_response = self.helper.invite( space_room_id, src=user2_id, targ=user1_id, tok=user2_tok ) # Have user2 leave the rooms to make sure that our background update is not just # reading from `current_state_events`. For invite/knock memberships, we should # be reading from the stripped state on the invite/knock event itself. self.helper.leave(room_id_no_info, user2_id, tok=user2_tok) self.helper.leave(room_id_with_info, user2_id, tok=user2_tok) self.helper.leave(space_room_id, user2_id, tok=user2_tok) # Check to make sure we actually don't have any `current_state_events` for the rooms current_state_check_rows = self.get_success( self.store.db_pool.simple_select_many_batch( table="current_state_events", column="room_id", iterable=[room_id_no_info, room_id_with_info, space_room_id], retcols=("event_id",), keyvalues={}, desc="check current_state_events in test", ) ) self.assertEqual(len(current_state_check_rows), 0) # Clean-up the `sliding_sync_membership_snapshots` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_membership_snapshots", column="room_id", iterable=(room_id_no_info, room_id_with_info, space_room_id), keyvalues={}, desc="sliding_sync_membership_snapshots.test_membership_snapshots_background_update_invite", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { # The invite memberships for user1 (room_id_no_info, user1_id), (room_id_with_info, user1_id), (space_room_id, user1_id), # The leave memberships for user2 (room_id_no_info, user2_id), (room_id_with_info, user2_id), (space_room_id, user2_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id_no_info, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id_no_info, user_id=user1_id, sender=user2_id, membership_event_id=user1_invite_room_id_no_info_response["event_id"], membership=Membership.INVITE, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_invite_room_id_no_info_response["event_id"] ) ).stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_with_info, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_with_info, user_id=user1_id, sender=user2_id, membership_event_id=user1_invite_room_id_with_info_response["event_id"], membership=Membership.INVITE, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_invite_room_id_with_info_response["event_id"] ) ).stream, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, # The tombstone isn't showing here ("another_room") because it's not one # of the stripped events that we hand out as part of the invite event. # Even though we handle this scenario from other remote homservers, # Synapse does not include the tombstone in the invite event. tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender=user2_id, membership_event_id=user1_invite_space_room_id_response["event_id"], membership=Membership.INVITE, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_invite_space_room_id_response["event_id"] ) ).stream, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_membership_snapshots_background_update_remote_invite( self, ) -> None: """ Test that the background update for `sliding_sync_membership_snapshots` backfills missing rows for remote invites (out-of-band memberships). """ user1_id = self.register_user("user1", "pass") _user1_tok = self.login(user1_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_unknown_state, room_id_unknown_state_invite_event = ( self._create_remote_invite_room_for_user(user1_id, None) ) room_id_no_info, room_id_no_info_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), ], ) ) room_id_with_info, room_id_with_info_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper room", }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), ], ) ) space_room_id, space_room_id_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, EventContentFields.ROOM_TYPE: RoomTypes.SPACE, }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper space", }, ), ], ) ) # Clean-up the `sliding_sync_membership_snapshots` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_membership_snapshots", column="room_id", iterable=( room_id_unknown_state, room_id_no_info, room_id_with_info, space_room_id, ), keyvalues={}, desc="sliding_sync_membership_snapshots.test_membership_snapshots_background_update_invite", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { # The invite memberships for user1 (room_id_unknown_state, user1_id), (room_id_no_info, user1_id), (room_id_with_info, user1_id), (space_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_unknown_state, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_unknown_state, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=room_id_unknown_state_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=room_id_unknown_state_invite_event.internal_metadata.stream_ordering, has_known_state=False, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id_no_info, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id_no_info, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=room_id_no_info_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=room_id_no_info_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_with_info, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_with_info, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=room_id_with_info_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=room_id_with_info_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=space_room_id_invite_event.event_id, membership=Membership.INVITE, event_stream_ordering=space_room_id_invite_event.internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) def test_membership_snapshots_background_update_remote_invite_rejections_and_retractions( self, ) -> None: """ Test that the background update for `sliding_sync_membership_snapshots` backfills missing rows for remote invite rejections/retractions (out-of-band memberships). """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_unknown_state, room_id_unknown_state_invite_event = ( self._create_remote_invite_room_for_user(user1_id, None) ) room_id_no_info, room_id_no_info_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), ], ) ) room_id_with_info, room_id_with_info_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper room", }, ), StrippedStateEvent( type=EventTypes.RoomEncryption, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2", }, ), ], ) ) space_room_id, space_room_id_invite_event = ( self._create_remote_invite_room_for_user( user1_id, [ StrippedStateEvent( type=EventTypes.Create, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_CREATOR: "@inviter:remote_server", EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier, EventContentFields.ROOM_TYPE: RoomTypes.SPACE, }, ), StrippedStateEvent( type=EventTypes.Name, state_key="", sender="@inviter:remote_server", content={ EventContentFields.ROOM_NAME: "my super duper space", }, ), ], ) ) # Reject the remote invites. # Also try retracting a remote invite. room_id_unknown_state_leave_event_response = self.helper.leave( room_id_unknown_state, user1_id, tok=user1_tok ) room_id_no_info_leave_event = self._retract_remote_invite_for_user( user_id=user1_id, remote_room_id=room_id_no_info, ) room_id_with_info_leave_event_response = self.helper.leave( room_id_with_info, user1_id, tok=user1_tok ) space_room_id_leave_event = self._retract_remote_invite_for_user( user_id=user1_id, remote_room_id=space_room_id, ) # Clean-up the `sliding_sync_membership_snapshots` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_membership_snapshots", column="room_id", iterable=( room_id_unknown_state, room_id_no_info, room_id_with_info, space_room_id, ), keyvalues={}, desc="sliding_sync_membership_snapshots.test_membership_snapshots_background_update_invite", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { # The invite memberships for user1 (room_id_unknown_state, user1_id), (room_id_no_info, user1_id), (room_id_with_info, user1_id), (space_room_id, user1_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_unknown_state, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_unknown_state, user_id=user1_id, sender=user1_id, membership_event_id=room_id_unknown_state_leave_event_response[ "event_id" ], membership=Membership.LEAVE, event_stream_ordering=self.get_success( self.store.get_position_for_event( room_id_unknown_state_leave_event_response["event_id"] ) ).stream, has_known_state=False, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id_no_info, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id_no_info, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=room_id_no_info_leave_event.event_id, membership=Membership.LEAVE, event_stream_ordering=room_id_no_info_leave_event.internal_metadata.stream_ordering, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_with_info, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_with_info, user_id=user1_id, sender=user1_id, membership_event_id=room_id_with_info_leave_event_response["event_id"], membership=Membership.LEAVE, event_stream_ordering=self.get_success( self.store.get_position_for_event( room_id_with_info_leave_event_response["event_id"] ) ).stream, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, sender="@inviter:remote_server", membership_event_id=space_room_id_leave_event.event_id, membership=Membership.LEAVE, event_stream_ordering=space_room_id_leave_event.internal_metadata.stream_ordering, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), ) @parameterized.expand( [ # We'll do a kick for this (Membership.LEAVE,), (Membership.BAN,), ] ) def test_membership_snapshots_background_update_historical_state( self, test_membership: str ) -> None: """ Test that the background update for `sliding_sync_membership_snapshots` backfills missing rows for leave memberships. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") # Create rooms with various levels of state that should appear in the table # room_id_no_info = self.helper.create_room_as(user2_id, tok=user2_tok) room_id_with_info = self.helper.create_room_as(user2_id, tok=user2_tok) # Add a room name self.helper.send_state( room_id_with_info, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # Encrypt the room self.helper.send_state( room_id_with_info, EventTypes.RoomEncryption, {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"}, tok=user2_tok, ) # Add a tombstone self.helper.send_state( room_id_with_info, EventTypes.Tombstone, {EventContentFields.TOMBSTONE_SUCCESSOR_ROOM: "another_room"}, tok=user2_tok, ) space_room_id = self.helper.create_room_as( user1_id, tok=user2_tok, extra_content={ "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} }, ) # Add a room name self.helper.send_state( space_room_id, EventTypes.Name, {"name": "my super duper space"}, tok=user2_tok, ) # Join the room in preparation for our test_membership self.helper.join(room_id_no_info, user1_id, tok=user1_tok) self.helper.join(room_id_with_info, user1_id, tok=user1_tok) self.helper.join(space_room_id, user1_id, tok=user1_tok) if test_membership == Membership.LEAVE: # Kick user1 from the rooms user1_membership_room_id_no_info_response = self.helper.change_membership( room=room_id_no_info, src=user2_id, targ=user1_id, tok=user2_tok, membership=Membership.LEAVE, extra_data={ "reason": "Bad manners", }, ) user1_membership_room_id_with_info_response = self.helper.change_membership( room=room_id_with_info, src=user2_id, targ=user1_id, tok=user2_tok, membership=Membership.LEAVE, extra_data={ "reason": "Bad manners", }, ) user1_membership_space_room_id_response = self.helper.change_membership( room=space_room_id, src=user2_id, targ=user1_id, tok=user2_tok, membership=Membership.LEAVE, extra_data={ "reason": "Bad manners", }, ) elif test_membership == Membership.BAN: # Ban user1 from the rooms user1_membership_room_id_no_info_response = self.helper.ban( room_id_no_info, src=user2_id, targ=user1_id, tok=user2_tok ) user1_membership_room_id_with_info_response = self.helper.ban( room_id_with_info, src=user2_id, targ=user1_id, tok=user2_tok ) user1_membership_space_room_id_response = self.helper.ban( space_room_id, src=user2_id, targ=user1_id, tok=user2_tok ) else: raise AssertionError("Unknown test_membership") # Have user2 leave the rooms to make sure that our background update is not just # reading from `current_state_events`. For leave memberships, we should be # reading from the historical state. self.helper.leave(room_id_no_info, user2_id, tok=user2_tok) self.helper.leave(room_id_with_info, user2_id, tok=user2_tok) self.helper.leave(space_room_id, user2_id, tok=user2_tok) # Check to make sure we actually don't have any `current_state_events` for the rooms current_state_check_rows = self.get_success( self.store.db_pool.simple_select_many_batch( table="current_state_events", column="room_id", iterable=[room_id_no_info, room_id_with_info, space_room_id], retcols=("event_id",), keyvalues={}, desc="check current_state_events in test", ) ) self.assertEqual(len(current_state_check_rows), 0) # Clean-up the `sliding_sync_membership_snapshots` table as if the inserts did not # happen during event creation. self.get_success( self.store.db_pool.simple_delete_many( table="sliding_sync_membership_snapshots", column="room_id", iterable=(room_id_no_info, room_id_with_info, space_room_id), keyvalues={}, desc="sliding_sync_membership_snapshots.test_membership_snapshots_background_update_invite", ) ) # We shouldn't find anything in the table because we just deleted them in # preparation for the test. sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), set(), exact=True, ) # Insert and run the background update. self.get_success( self.store.db_pool.simple_insert( "background_updates", { "update_name": _BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BACKFILL, "progress_json": "{}", }, ) ) self.store.db_pool.updates._all_done = False self.wait_for_background_updates() # Make sure the table is populated sliding_sync_membership_snapshots_results = ( self._get_sliding_sync_membership_snapshots() ) self.assertIncludes( set(sliding_sync_membership_snapshots_results.keys()), { # The memberships for user1 (room_id_no_info, user1_id), (room_id_with_info, user1_id), (space_room_id, user1_id), # The leave memberships for user2 (room_id_no_info, user2_id), (room_id_with_info, user2_id), (space_room_id, user2_id), }, exact=True, ) self.assertEqual( sliding_sync_membership_snapshots_results.get((room_id_no_info, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=room_id_no_info, user_id=user1_id, # Because user2 kicked/banned user1 from the room sender=user2_id, membership_event_id=user1_membership_room_id_no_info_response[ "event_id" ], membership=test_membership, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_membership_room_id_no_info_response["event_id"] ) ).stream, has_known_state=True, room_type=None, room_name=None, is_encrypted=False, tombstone_successor_room_id=None, ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get( (room_id_with_info, user1_id) ), _SlidingSyncMembershipSnapshotResult( room_id=room_id_with_info, user_id=user1_id, # Because user2 kicked/banned user1 from the room sender=user2_id, membership_event_id=user1_membership_room_id_with_info_response[ "event_id" ], membership=test_membership, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_membership_room_id_with_info_response["event_id"] ) ).stream, has_known_state=True, room_type=None, room_name="my super duper room", is_encrypted=True, tombstone_successor_room_id="another_room", ), ) self.assertEqual( sliding_sync_membership_snapshots_results.get((space_room_id, user1_id)), _SlidingSyncMembershipSnapshotResult( room_id=space_room_id, user_id=user1_id, # Because user2 kicked/banned user1 from the room sender=user2_id, membership_event_id=user1_membership_space_room_id_response["event_id"], membership=test_membership, event_stream_ordering=self.get_success( self.store.get_position_for_event( user1_membership_space_room_id_response["event_id"] ) ).stream, has_known_state=True, room_type=RoomTypes.SPACE, room_name="my super duper space", is_encrypted=False, tombstone_successor_room_id=None, ), )