summary refs log tree commit diff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/handlers/test_room_summary.py4
-rw-r--r--tests/handlers/test_space_hierarchy.py239
-rw-r--r--tests/rest/admin/test_space.py399
3 files changed, 640 insertions, 2 deletions
diff --git a/tests/handlers/test_room_summary.py b/tests/handlers/test_room_summary.py

index e5a6a6c747..e85d112ecc 100644 --- a/tests/handlers/test_room_summary.py +++ b/tests/handlers/test_room_summary.py
@@ -28,7 +28,7 @@ from synapse.api.constants import ( from synapse.api.errors import AuthError, NotFoundError, SynapseError from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict -from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry +from synapse.handlers.room_summary import _RoomEntry, child_events_comparison_key from synapse.rest import admin from synapse.rest.client import login, room from synapse.server import HomeServer @@ -48,7 +48,7 @@ def _create_event(room_id: str, order: Optional[Any] = None, origin_server_ts: i def _order(*events): - return sorted(events, key=_child_events_comparison_key) + return sorted(events, key=child_events_comparison_key) class TestSpaceSummarySort(unittest.TestCase): diff --git a/tests/handlers/test_space_hierarchy.py b/tests/handlers/test_space_hierarchy.py new file mode 100644
index 0000000000..63bc93d558 --- /dev/null +++ b/tests/handlers/test_space_hierarchy.py
@@ -0,0 +1,239 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Iterable, Mapping, NoReturn, Optional, Sequence, Tuple +from unittest import mock + +from twisted.test.proto_helpers import MemoryReactor + +from synapse.api.constants import EventContentFields, EventTypes, RoomTypes +from synapse.handlers.space_hierarchy import SpaceHierarchyHandler +from synapse.rest import admin +from synapse.rest.client import login, room +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util import Clock + +from tests import unittest + + +class SpaceDescendantsTestCase(unittest.HomeserverTestCase): + """Tests iteration over the descendants of a space.""" + + servlets = [ + admin.register_servlets_for_client_rest_resource, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer): + self.hs = hs + self.handler = self.hs.get_space_hierarchy_handler() + + # Create a user. + self.user = self.register_user("user", "pass") + self.token = self.login("user", "pass") + + # Create a space and a child room. + self.space = self.helper.create_room_as( + self.user, + tok=self.token, + extra_content={ + "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} + }, + ) + self.room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(self.space, self.room) + + def _add_child( + self, space_id: str, room_id: str, order: Optional[str] = None + ) -> None: + """Adds a room to a space.""" + content: JsonDict = {"via": [self.hs.hostname]} + if order is not None: + content["order"] = order + self.helper.send_state( + space_id, + event_type=EventTypes.SpaceChild, + body=content, + tok=self.token, + state_key=room_id, + ) + + def _create_space(self) -> str: + """Creates a space.""" + return self._create_room( + extra_content={ + "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} + }, + ) + + def _create_room(self, extra_content: Optional[Dict] = None) -> str: + """Creates a room.""" + return self.helper.create_room_as( + self.user, + tok=self.token, + extra_content=extra_content, + ) + + def test_empty_space(self): + """Tests iteration over an empty space.""" + space_id = self._create_space() + + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants(space_id) + ) + + self.assertEqual(descendants, [(space_id, [])]) + self.assertEqual(inaccessible_room_ids, []) + + def test_invalid_space(self): + """Tests iteration over an inaccessible space.""" + space_id = f"!invalid:{self.hs.hostname}" + + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants(space_id) + ) + + self.assertEqual(descendants, [(space_id, [])]) + self.assertEqual(inaccessible_room_ids, [space_id]) + + def test_invalid_room(self): + """Tests iteration over a space containing an inaccessible room.""" + space_id = self._create_space() + room_id = f"!invalid:{self.hs.hostname}" + self._add_child(space_id, room_id) + + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants(space_id) + ) + + self.assertEqual(descendants, [(space_id, []), (room_id, [self.hs.hostname])]) + self.assertEqual(inaccessible_room_ids, [room_id]) + + def test_remote_space_with_federation_enabled(self): + """Tests iteration over a remote space with federation enabled.""" + space_id = "!space:remote" + room_id = "!room:remote" + + async def _get_space_children_remote( + _self: SpaceHierarchyHandler, space_id: str, via: Iterable[str] + ) -> Tuple[ + Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]] + ]: + if space_id == "!space:remote": + self.assertEqual(via, ["remote"]) + return [("!room:remote", ["remote"])], {} + elif space_id == "!room:remote": + self.assertEqual(via, ["remote"]) + return [], {} + else: + self.fail( + f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call" + ) + raise # `fail` is missing type hints + + with mock.patch( + "synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote", + new=_get_space_children_remote, + ): + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants( + space_id, via=["remote"], enable_federation=True + ) + ) + + self.assertEqual(descendants, [(space_id, ["remote"]), (room_id, ["remote"])]) + self.assertEqual(inaccessible_room_ids, [space_id, room_id]) + + def test_remote_space_with_federation_disabled(self): + """Tests iteration over a remote space with federation disabled.""" + space_id = "!space:remote" + + async def _get_space_children_remote( + _self: SpaceHierarchyHandler, space_id: str, via: Iterable[str] + ) -> NoReturn: + self.fail( + f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call" + ) + raise # `fail` is missing type hints + + with mock.patch( + "synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote", + new=_get_space_children_remote, + ): + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants( + space_id, via=["remote"], enable_federation=False + ) + ) + + self.assertEqual(descendants, [(space_id, ["remote"])]) + self.assertEqual(inaccessible_room_ids, [space_id]) + + def test_cycle(self): + """Tests iteration over a cyclic space.""" + # space_id + # - subspace_id + # - space_id + space_id = self._create_space() + subspace_id = self._create_space() + self._add_child(space_id, subspace_id) + self._add_child(subspace_id, space_id) + + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants(space_id) + ) + + self.assertEqual( + descendants, [(space_id, []), (subspace_id, [self.hs.hostname])] + ) + self.assertEqual(inaccessible_room_ids, []) + + def test_duplicates(self): + """Tests iteration over a space with repeated rooms.""" + # space_id + # - subspace_id + # - duplicate_room_1_id + # - duplicate_room_2_id + # - room_id + # - duplicate_room_1_id + # - duplicate_room_2_id + space_id = self._create_space() + subspace_id = self._create_space() + room_id = self._create_room() + duplicate_room_1_id = self._create_room() + duplicate_room_2_id = self._create_room() + self._add_child(space_id, subspace_id, order="1") + self._add_child(space_id, duplicate_room_1_id, order="2") + self._add_child(space_id, duplicate_room_2_id, order="3") + self._add_child(subspace_id, duplicate_room_1_id, order="1") + self._add_child(subspace_id, duplicate_room_2_id, order="2") + self._add_child(subspace_id, room_id, order="3") + + descendants, inaccessible_room_ids = self.get_success( + self.handler.get_space_descendants(space_id) + ) + + self.assertEqual( + descendants, + [ + (space_id, []), + (subspace_id, [self.hs.hostname]), + (room_id, [self.hs.hostname]), + (duplicate_room_1_id, [self.hs.hostname]), + (duplicate_room_2_id, [self.hs.hostname]), + ], + ) + self.assertEqual(inaccessible_room_ids, []) diff --git a/tests/rest/admin/test_space.py b/tests/rest/admin/test_space.py new file mode 100644
index 0000000000..2aa5a26142 --- /dev/null +++ b/tests/rest/admin/test_space.py
@@ -0,0 +1,399 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import ( + Dict, + Iterable, + List, + Mapping, + NoReturn, + Optional, + Sequence, + Tuple, + Union, +) +from unittest import mock + +from typing_extensions import Literal + +from twisted.test.proto_helpers import MemoryReactor + +import synapse.rest.admin +from synapse.api.constants import ( + EventContentFields, + EventTypes, + JoinRules, + Membership, + RestrictedJoinRuleTypes, + RoomTypes, +) +from synapse.api.room_versions import RoomVersions +from synapse.handlers.space_hierarchy import SpaceHierarchyHandler +from synapse.rest.client import login, room +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util import Clock + +from tests import unittest + + +class RemoveSpaceMemberTestCase(unittest.HomeserverTestCase): + """Tests removal of a user from a space.""" + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer): + self.store = hs.get_datastore() + + # Create users + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + self.space_owner_user = self.register_user("space_owner", "pass") + self.space_owner_user_tok = self.login("space_owner", "pass") + self.target_user = self.register_user("user", "pass") + self.target_user_tok = self.login("user", "pass") + + # Create a space hierarchy for testing: + # space, invite-only + # * subspace, restricted + self.space_id = self._create_space(JoinRules.INVITE) + + # Make the target user a member of the space + self.helper.invite( + self.space_id, + src=self.space_owner_user, + targ=self.target_user, + tok=self.space_owner_user_tok, + ) + self.helper.join(self.space_id, self.target_user, tok=self.target_user_tok) + + self.subspace_id = self._create_space((JoinRules.RESTRICTED, self.space_id)) + self._add_child(self.space_id, self.subspace_id) + + def _add_child( + self, space_id: str, room_id: str, via: Optional[List[str]] = None + ) -> None: + """Adds a room to a space.""" + if via is None: + via = [self.hs.hostname] + + self.helper.send_state( + space_id, + event_type=EventTypes.SpaceChild, + body={"via": via}, + tok=self.space_owner_user_tok, + state_key=room_id, + ) + + def _create_space( + self, + join_rules: Union[ + Literal["public", "invite", "knock"], + Tuple[Literal["restricted"], str], + ], + ) -> str: + """Creates a space.""" + return self._create_room( + join_rules, + extra_content={ + "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} + }, + ) + + def _create_room( + self, + join_rules: Union[ + Literal["public", "invite", "knock"], + Tuple[Literal["restricted"], str], + ], + extra_content: Optional[Dict] = None, + ) -> str: + """Creates a room.""" + room_id = self.helper.create_room_as( + self.space_owner_user, + room_version=RoomVersions.V8.identifier, + tok=self.space_owner_user_tok, + extra_content=extra_content, + ) + + if isinstance(join_rules, str): + self.helper.send_state( + room_id, + event_type=EventTypes.JoinRules, + body={"join_rule": join_rules}, + tok=self.space_owner_user_tok, + ) + else: + _, space_id = join_rules + self.helper.send_state( + room_id, + event_type=EventTypes.JoinRules, + body={ + "join_rule": JoinRules.RESTRICTED, + "allow": [ + { + "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP, + "room_id": space_id, + "via": [self.hs.hostname], + } + ], + }, + tok=self.space_owner_user_tok, + ) + + return room_id + + def _remove_from_space( + self, + user_id: str, + space_id: Optional[str] = None, + include_remote_spaces: Optional[bool] = None, + ) -> JsonDict: + """Removes the given user from the test space.""" + if space_id is None: + space_id = self.space_id + + content: Union[bytes, JsonDict] = b"" + if include_remote_spaces is not None: + content = {"include_remote_spaces": include_remote_spaces} + + url = f"/_synapse/admin/v1/rooms/{self.space_id}/hierarchy/members/{user_id}" + channel = self.make_request( + "DELETE", + url.encode("ascii"), + access_token=self.admin_user_tok, + content=content, + ) + + self.assertEqual(200, channel.code, channel.json_body) + + return channel.json_body + + def test_public_space(self) -> None: + """Tests that the user is removed from the space, even if public.""" + self.helper.send_state( + self.space_id, + event_type=EventTypes.JoinRules, + body={"join_rule": JoinRules.PUBLIC}, + tok=self.space_owner_user_tok, + ) + + response = self._remove_from_space(self.target_user) + self.assertEqual( + response, + { + "left_rooms": [self.space_id], + "inaccessible_rooms": [], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, self.space_id + ) + ) + self.assertEqual(membership, Membership.LEAVE) + + def test_public_room(self) -> None: + """Tests that the user is not removed from public rooms.""" + public_room_id = self._create_room(JoinRules.PUBLIC) + self._add_child(self.subspace_id, public_room_id) + + self.helper.join(public_room_id, self.target_user, tok=self.target_user_tok) + + response = self._remove_from_space(self.target_user) + self.assertEqual( + response, + { + "left_rooms": [self.space_id], + "inaccessible_rooms": [], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, public_room_id + ) + ) + self.assertEqual(membership, Membership.JOIN) + + def test_invited(self) -> None: + """Tests that the user is made to decline invites to rooms in the space.""" + invite_only_room_id = self._create_room(JoinRules.INVITE) + self._add_child(self.subspace_id, invite_only_room_id) + + self.helper.invite( + invite_only_room_id, + src=self.space_owner_user, + targ=self.target_user, + tok=self.space_owner_user_tok, + ) + + response = self._remove_from_space(self.target_user) + self.assertEqual( + response, + { + "left_rooms": [self.space_id, invite_only_room_id], + "inaccessible_rooms": [], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, invite_only_room_id + ) + ) + self.assertEqual(membership, Membership.LEAVE) + + def test_invite_only_room(self) -> None: + """Tests that the user is made to leave invite-only rooms.""" + invite_only_room_id = self._create_room(JoinRules.INVITE) + self._add_child(self.subspace_id, invite_only_room_id) + + self.helper.invite( + invite_only_room_id, + src=self.space_owner_user, + targ=self.target_user, + tok=self.space_owner_user_tok, + ) + self.helper.join( + invite_only_room_id, self.target_user, tok=self.target_user_tok + ) + + response = self._remove_from_space(self.target_user) + self.assertEqual( + response, + { + "left_rooms": [self.space_id, invite_only_room_id], + "inaccessible_rooms": [], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, invite_only_room_id + ) + ) + self.assertEqual(membership, Membership.LEAVE) + + def test_restricted_room(self) -> None: + """Tests that the user is made to leave restricted rooms.""" + restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id)) + self._add_child(self.subspace_id, restricted_room_id) + self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok) + + response = self._remove_from_space(self.target_user) + self.assertEqual( + response, + { + "left_rooms": [self.space_id, restricted_room_id], + "inaccessible_rooms": [], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, restricted_room_id + ) + ) + self.assertEqual(membership, Membership.LEAVE) + + def test_remote_space(self) -> None: + """Tests that the user is made to leave rooms in a remote space.""" + remote_space_id = "!space:remote" + self._add_child(self.subspace_id, remote_space_id, via=["remote"]) + + restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id)) + self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok) + + async def _get_space_children_remote( + _self: SpaceHierarchyHandler, space_id: str, via: Iterable[str] + ) -> Tuple[ + Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]] + ]: + self.assertEqual(space_id, remote_space_id) + self.assertEqual(via, ["remote"]) + + return [(restricted_room_id, [self.hs.hostname])], {} + + with mock.patch( + "synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote", + new=_get_space_children_remote, + ): + response = self._remove_from_space( + self.target_user, space_id="!space:remote", include_remote_spaces=True + ) + self.assertEqual( + response, + { + "left_rooms": [self.space_id, restricted_room_id], + "inaccessible_rooms": [remote_space_id], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, restricted_room_id + ) + ) + self.assertEqual(membership, Membership.LEAVE) + + def test_remote_spaces_excluded(self) -> None: + """Tests the exclusion of remote spaces.""" + remote_space_id = "!space:remote" + self._add_child(self.subspace_id, remote_space_id, via=["remote"]) + + restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id)) + self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok) + + async def _get_space_children_remote( + _self: SpaceHierarchyHandler, space_id: str, via: Iterable[str] + ) -> NoReturn: + self.fail( + f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call" + ) + raise # `fail` is missing type hints + + with mock.patch( + "synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote", + new=_get_space_children_remote, + ): + response = self._remove_from_space( + self.target_user, space_id="!space:remote", include_remote_spaces=False + ) + self.assertEqual( + response, + { + "left_rooms": [self.space_id], + "inaccessible_rooms": [remote_space_id], + "failed_rooms": {}, + }, + ) + + membership, _ = self.get_success( + self.store.get_local_current_membership_for_user_in_room( + self.target_user, restricted_room_id + ) + ) + self.assertEqual(membership, Membership.JOIN)