diff --git a/synapse/handlers/sliding_sync.py b/synapse/handlers/sliding_sync.py index 649167852c..1f9dcc9957 100644 --- a/synapse/handlers/sliding_sync.py +++ b/synapse/handlers/sliding_sync.py @@ -25,6 +25,7 @@ class SlidingSyncConfig(SlidingSyncBody): user: UserID device_id: str + # Pydantic config class Config: # By default, ignore fields that we don't recognise. extra = Extra.ignore @@ -186,7 +187,7 @@ class SlidingSyncHandler: # See https://github.com/matrix-org/matrix-doc/issues/1144 raise NotImplementedError() - room_id_list = await self.get_current_room_ids_for_user( + room_id_list = await self.get_sync_room_ids_for_user( sync_config.user, from_token=from_token, to_token=to_token, @@ -198,15 +199,17 @@ class SlidingSyncHandler: # TODO: Calculate Membership changes between the last sync and the current sync. - async def get_current_room_ids_for_user( + async def get_sync_room_ids_for_user( self, user: UserID, from_token: Optional[StreamToken] = None, to_token: StreamToken = None, ) -> AbstractSet[str]: """ - Fetch room IDs that the user has not left or newly_left rooms < `from_token` and - <= `to_token`. + Fetch room IDs that should be listed for this user in the sync response. + + We're looking for rooms that the user has not left or newly_left rooms that are + > `from_token` and <= `to_token`. """ user_id = user.to_string() @@ -230,9 +233,10 @@ class SlidingSyncHandler: room_for_user.room_id for room_for_user in room_for_user_list } - # If our `to_token` is already ahead of the latest room membership for the user, - # we can just straight up return the room list (nothing has changed) - if max_stream_ordering_from_room_list < to_token.room_key.stream: + # If our `to_token` is already the same or ahead of the latest room membership + # for the user, we can just straight up return the room list (nothing has + # changed) + if max_stream_ordering_from_room_list <= to_token.room_key.stream: return sync_room_id_set # We assume the `from_token` is before the `to_token` diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 1516689853..a047cd7b7a 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -777,6 +777,10 @@ class SlidingSyncRestServlet(RestServlet): sync_config = SlidingSyncConfig( user=user, device_id=device_id, + # FIXME: Currently, we're just manually copying the fields from the + # `SlidingSyncBody` into the config. How can we gurantee into the future + # that we don't forget any? I would like something more structured like + # `copy_attributes(from=body, to=config)` lists=body.lists, room_subscriptions=body.room_subscriptions, extensions=body.extensions, diff --git a/tests/handlers/test_sliding_sync.py b/tests/handlers/test_sliding_sync.py new file mode 100644 index 0000000000..4659dd84e6 --- /dev/null +++ b/tests/handlers/test_sliding_sync.py @@ -0,0 +1,155 @@ +from twisted.test.proto_helpers import MemoryReactor + +from synapse.api.constants import EventTypes, JoinRules +from synapse.api.room_versions import RoomVersions +from synapse.rest import admin +from synapse.rest.client import knock, login, room +from synapse.server import HomeServer +from synapse.types import JsonDict, UserID +from synapse.util import Clock + +from tests.unittest import HomeserverTestCase + + +class GetSyncRoomIdsForUserTestCase(HomeserverTestCase): + """Tests Sliding Sync handler `get_sync_room_ids_for_user`.""" + + servlets = [ + admin.register_servlets, + knock.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def default_config(self) -> JsonDict: + config = super().default_config() + # Enable sliding sync + config["experimental_features"] = {"msc3575_enabled": True} + return config + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + self.sliding_sync_handler = self.hs.get_sliding_sync_handler() + self.store = self.hs.get_datastores().main + self.event_sources = hs.get_event_sources() + + def test_get_newly_joined_room(self) -> None: + user1_id = self.register_user("user1", "pass") + user1_tok = self.login(user1_id, "pass") + + before_room_token = self.event_sources.get_current_token() + + room_id = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True) + + after_room_token = self.event_sources.get_current_token() + + room_id_results = self.get_success( + self.sliding_sync_handler.get_sync_room_ids_for_user( + UserID.from_string(user1_id), + from_token=before_room_token, + to_token=after_room_token, + ) + ) + + self.assertEqual(room_id_results, {room_id}) + + def test_get_already_joined_room(self) -> None: + user1_id = self.register_user("user1", "pass") + user1_tok = self.login(user1_id, "pass") + + room_id = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True) + + after_room_token = self.event_sources.get_current_token() + + room_id_results = self.get_success( + self.sliding_sync_handler.get_sync_room_ids_for_user( + UserID.from_string(user1_id), + from_token=after_room_token, + to_token=after_room_token, + ) + ) + + self.assertEqual(room_id_results, {room_id}) + + def test_get_invited_banned_knocked_room(self) -> None: + 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") + + before_room_token = self.event_sources.get_current_token() + + # Setup the invited room (user2 invites user1 to the room) + invited_room_id = self.helper.create_room_as(user2_id, tok=user2_tok) + self.helper.invite(invited_room_id, targ=user1_id, tok=user2_tok) + + # Setup the ban room (user2 bans user1 from the room) + ban_room_id = self.helper.create_room_as( + user2_id, tok=user2_tok, is_public=True + ) + self.helper.join(ban_room_id, user1_id, tok=user1_tok) + self.helper.ban(ban_room_id, src=user2_id, targ=user1_id, tok=user2_tok) + + # Setup the knock room (user1 knocks on the room) + knock_room_id = self.helper.create_room_as( + user2_id, tok=user2_tok, room_version=RoomVersions.V7.identifier + ) + self.helper.send_state( + knock_room_id, + EventTypes.JoinRules, + {"join_rule": JoinRules.KNOCK}, + tok=user2_tok, + ) + # User1 knocks on the room + channel = self.make_request( + "POST", + "/_matrix/client/r0/knock/%s" % (knock_room_id,), + b"{}", + user1_tok, + ) + self.assertEqual(200, channel.code, channel.result) + + after_room_token = self.event_sources.get_current_token() + + room_id_results = self.get_success( + self.sliding_sync_handler.get_sync_room_ids_for_user( + UserID.from_string(user1_id), + from_token=before_room_token, + to_token=after_room_token, + ) + ) + + # Ensure that the invited, ban, and knock rooms show up + self.assertEqual( + room_id_results, + { + invited_room_id, + ban_room_id, + knock_room_id, + }, + ) + + def test_only_newly_left_rooms_show_up(self) -> None: + user1_id = self.register_user("user1", "pass") + user1_tok = self.login(user1_id, "pass") + + # Leave before we calculate the `from_token` + room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok) + self.helper.leave(room_id1, user1_id, tok=user1_tok) + + before_room_token = self.event_sources.get_current_token() + + # Leave during the from_token/to_token range (newly_left) + room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok) + self.helper.leave(room_id1, user1_id, tok=user1_tok) + + after_room_token = self.event_sources.get_current_token() + + room_id_results = self.get_success( + self.sliding_sync_handler.get_sync_room_ids_for_user( + UserID.from_string(user1_id), + from_token=before_room_token, + to_token=after_room_token, + ) + ) + + self.assertEqual(room_id_results, {room_id2})