# # This file is licensed under the Affero General Public License (AGPL) version 3. # # Copyright (C) 2024 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: # . # import logging from parameterized import parameterized_class from twisted.test.proto_helpers import MemoryReactor import synapse.rest.admin from synapse.api.constants import EventTypes, HistoryVisibility from synapse.rest.client import login, room, sync from synapse.server import HomeServer from synapse.types import UserID from synapse.util import Clock from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase logger = logging.getLogger(__name__) # FIXME: This can be removed once we bump `SCHEMA_COMPAT_VERSION` and run the # foreground update for # `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots` (tracked by # https://github.com/element-hq/synapse/issues/17623) @parameterized_class( ("use_new_tables",), [ (True,), (False,), ], class_name_func=lambda cls, num, params_dict: f"{cls.__name__}_{'new' if params_dict['use_new_tables'] else 'fallback'}", ) class SlidingSyncRoomsInvitesTestCase(SlidingSyncBase): """ Test to make sure the `rooms` response looks good for invites in the Sliding Sync API. Invites behave a lot different than other rooms because we don't include the `timeline` (`num_live`, `limited`, `prev_batch`) or `required_state` in favor of some stripped state under the `invite_state` key. Knocks probably have the same behavior but the spec doesn't mention knocks yet. """ servlets = [ synapse.rest.admin.register_servlets, login.register_servlets, room.register_servlets, sync.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() super().prepare(reactor, clock, hs) def test_rooms_invite_shared_history_initial_sync(self) -> None: """ Test that `rooms` we are invited to have some stripped `invite_state` during an initial sync. This is an `invite` room so we should only have `stripped_state` (no `timeline`) but we also shouldn't see any timeline events because the history visiblity is `shared` and we haven't joined the room yet. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user1 = UserID.from_string(user1_id) user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user2 = UserID.from_string(user2_id) room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # Ensure we're testing with a room with `shared` history visibility which means # history visible until you actually join the room. history_visibility_response = self.helper.get_state( room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok ) self.assertEqual( history_visibility_response.get("history_visibility"), HistoryVisibility.SHARED, ) self.helper.send(room_id1, "activity before1", tok=user2_tok) self.helper.send(room_id1, "activity before2", tok=user2_tok) self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) self.helper.send(room_id1, "activity after3", tok=user2_tok) self.helper.send(room_id1, "activity after4", tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 3, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # `timeline` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("timeline"), response_body["rooms"][room_id1], ) # `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("num_live"), response_body["rooms"][room_id1], ) # `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("limited"), response_body["rooms"][room_id1], ) # `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("prev_batch"), response_body["rooms"][room_id1], ) # `required_state` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("required_state"), response_body["rooms"][room_id1], ) # We should have some `stripped_state` so the potential joiner can identify the # room (we don't care about the order). self.assertCountEqual( response_body["rooms"][room_id1]["invite_state"], [ { "content": {"creator": user2_id, "room_version": "10"}, "sender": user2_id, "state_key": "", "type": "m.room.create", }, { "content": {"join_rule": "public"}, "sender": user2_id, "state_key": "", "type": "m.room.join_rules", }, { "content": {"displayname": user2.localpart, "membership": "join"}, "sender": user2_id, "state_key": user2_id, "type": "m.room.member", }, { "content": {"displayname": user1.localpart, "membership": "invite"}, "sender": user2_id, "state_key": user1_id, "type": "m.room.member", }, ], response_body["rooms"][room_id1]["invite_state"], ) def test_rooms_invite_shared_history_incremental_sync(self) -> None: """ Test that `rooms` we are invited to have some stripped `invite_state` during an incremental sync. This is an `invite` room so we should only have `stripped_state` (no `timeline`) but we also shouldn't see any timeline events because the history visiblity is `shared` and we haven't joined the room yet. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user1 = UserID.from_string(user1_id) user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user2 = UserID.from_string(user2_id) room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok) # Ensure we're testing with a room with `shared` history visibility which means # history visible until you actually join the room. history_visibility_response = self.helper.get_state( room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok ) self.assertEqual( history_visibility_response.get("history_visibility"), HistoryVisibility.SHARED, ) self.helper.send(room_id1, "activity before invite1", tok=user2_tok) self.helper.send(room_id1, "activity before invite2", tok=user2_tok) self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) self.helper.send(room_id1, "activity after invite3", tok=user2_tok) self.helper.send(room_id1, "activity after invite4", tok=user2_tok) sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 3, } } } _, from_token = self.do_sync(sync_body, tok=user1_tok) self.helper.send(room_id1, "activity after token5", tok=user2_tok) self.helper.send(room_id1, "activity after toekn6", tok=user2_tok) # Make the Sliding Sync request response_body, from_token = self.do_sync( sync_body, since=from_token, tok=user1_tok ) # `timeline` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("timeline"), response_body["rooms"][room_id1], ) # `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("num_live"), response_body["rooms"][room_id1], ) # `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("limited"), response_body["rooms"][room_id1], ) # `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("prev_batch"), response_body["rooms"][room_id1], ) # `required_state` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("required_state"), response_body["rooms"][room_id1], ) # We should have some `stripped_state` so the potential joiner can identify the # room (we don't care about the order). self.assertCountEqual( response_body["rooms"][room_id1]["invite_state"], [ { "content": {"creator": user2_id, "room_version": "10"}, "sender": user2_id, "state_key": "", "type": "m.room.create", }, { "content": {"join_rule": "public"}, "sender": user2_id, "state_key": "", "type": "m.room.join_rules", }, { "content": {"displayname": user2.localpart, "membership": "join"}, "sender": user2_id, "state_key": user2_id, "type": "m.room.member", }, { "content": {"displayname": user1.localpart, "membership": "invite"}, "sender": user2_id, "state_key": user1_id, "type": "m.room.member", }, ], response_body["rooms"][room_id1]["invite_state"], ) def test_rooms_invite_world_readable_history_initial_sync(self) -> None: """ Test that `rooms` we are invited to have some stripped `invite_state` during an initial sync. This is an `invite` room so we should only have `stripped_state` (no `timeline`) but depending on the semantics we decide, we could potentially see some historical events before/after the `from_token` because the history is `world_readable`. Same situation for events after the `from_token` if the history visibility was set to `invited`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user1 = UserID.from_string(user1_id) user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user2 = UserID.from_string(user2_id) room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "preset": "public_chat", "initial_state": [ { "content": { "history_visibility": HistoryVisibility.WORLD_READABLE }, "state_key": "", "type": EventTypes.RoomHistoryVisibility, } ], }, ) # Ensure we're testing with a room with `world_readable` history visibility # which means events are visible to anyone even without membership. history_visibility_response = self.helper.get_state( room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok ) self.assertEqual( history_visibility_response.get("history_visibility"), HistoryVisibility.WORLD_READABLE, ) self.helper.send(room_id1, "activity before1", tok=user2_tok) self.helper.send(room_id1, "activity before2", tok=user2_tok) self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) self.helper.send(room_id1, "activity after3", tok=user2_tok) self.helper.send(room_id1, "activity after4", tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], # Large enough to see the latest events and before the invite "timeline_limit": 4, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # `timeline` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("timeline"), response_body["rooms"][room_id1], ) # `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("num_live"), response_body["rooms"][room_id1], ) # `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("limited"), response_body["rooms"][room_id1], ) # `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("prev_batch"), response_body["rooms"][room_id1], ) # `required_state` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("required_state"), response_body["rooms"][room_id1], ) # We should have some `stripped_state` so the potential joiner can identify the # room (we don't care about the order). self.assertCountEqual( response_body["rooms"][room_id1]["invite_state"], [ { "content": {"creator": user2_id, "room_version": "10"}, "sender": user2_id, "state_key": "", "type": "m.room.create", }, { "content": {"join_rule": "public"}, "sender": user2_id, "state_key": "", "type": "m.room.join_rules", }, { "content": {"displayname": user2.localpart, "membership": "join"}, "sender": user2_id, "state_key": user2_id, "type": "m.room.member", }, { "content": {"displayname": user1.localpart, "membership": "invite"}, "sender": user2_id, "state_key": user1_id, "type": "m.room.member", }, ], response_body["rooms"][room_id1]["invite_state"], ) def test_rooms_invite_world_readable_history_incremental_sync(self) -> None: """ Test that `rooms` we are invited to have some stripped `invite_state` during an incremental sync. This is an `invite` room so we should only have `stripped_state` (no `timeline`) but depending on the semantics we decide, we could potentially see some historical events before/after the `from_token` because the history is `world_readable`. Same situation for events after the `from_token` if the history visibility was set to `invited`. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user1 = UserID.from_string(user1_id) user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user2 = UserID.from_string(user2_id) room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "preset": "public_chat", "initial_state": [ { "content": { "history_visibility": HistoryVisibility.WORLD_READABLE }, "state_key": "", "type": EventTypes.RoomHistoryVisibility, } ], }, ) # Ensure we're testing with a room with `world_readable` history visibility # which means events are visible to anyone even without membership. history_visibility_response = self.helper.get_state( room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok ) self.assertEqual( history_visibility_response.get("history_visibility"), HistoryVisibility.WORLD_READABLE, ) self.helper.send(room_id1, "activity before invite1", tok=user2_tok) self.helper.send(room_id1, "activity before invite2", tok=user2_tok) self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) self.helper.send(room_id1, "activity after invite3", tok=user2_tok) self.helper.send(room_id1, "activity after invite4", tok=user2_tok) sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], # Large enough to see the latest events and before the invite "timeline_limit": 4, } } } _, from_token = self.do_sync(sync_body, tok=user1_tok) self.helper.send(room_id1, "activity after token5", tok=user2_tok) self.helper.send(room_id1, "activity after toekn6", tok=user2_tok) # Make the incremental Sliding Sync request response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok) # `timeline` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("timeline"), response_body["rooms"][room_id1], ) # `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("num_live"), response_body["rooms"][room_id1], ) # `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("limited"), response_body["rooms"][room_id1], ) # `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway) self.assertIsNone( response_body["rooms"][room_id1].get("prev_batch"), response_body["rooms"][room_id1], ) # `required_state` is omitted for `invite` rooms with `stripped_state` self.assertIsNone( response_body["rooms"][room_id1].get("required_state"), response_body["rooms"][room_id1], ) # We should have some `stripped_state` so the potential joiner can identify the # room (we don't care about the order). self.assertCountEqual( response_body["rooms"][room_id1]["invite_state"], [ { "content": {"creator": user2_id, "room_version": "10"}, "sender": user2_id, "state_key": "", "type": "m.room.create", }, { "content": {"join_rule": "public"}, "sender": user2_id, "state_key": "", "type": "m.room.join_rules", }, { "content": {"displayname": user2.localpart, "membership": "join"}, "sender": user2_id, "state_key": user2_id, "type": "m.room.member", }, { "content": {"displayname": user1.localpart, "membership": "invite"}, "sender": user2_id, "state_key": user1_id, "type": "m.room.member", }, ], response_body["rooms"][room_id1]["invite_state"], )