573 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			573 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
| #  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 Any, Iterable, Optional, Tuple
 | |
| from unittest import mock
 | |
| 
 | |
| from synapse.api.constants import (
 | |
|     EventContentFields,
 | |
|     EventTypes,
 | |
|     HistoryVisibility,
 | |
|     JoinRules,
 | |
|     Membership,
 | |
|     RestrictedJoinRuleTypes,
 | |
|     RoomTypes,
 | |
| )
 | |
| from synapse.api.errors import AuthError
 | |
| from synapse.api.room_versions import RoomVersions
 | |
| from synapse.events import make_event_from_dict
 | |
| from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry
 | |
| from synapse.rest import admin
 | |
| from synapse.rest.client.v1 import login, room
 | |
| from synapse.server import HomeServer
 | |
| from synapse.types import JsonDict
 | |
| 
 | |
| from tests import unittest
 | |
| 
 | |
| 
 | |
| def _create_event(room_id: str, order: Optional[Any] = None):
 | |
|     result = mock.Mock()
 | |
|     result.room_id = room_id
 | |
|     result.content = {}
 | |
|     if order is not None:
 | |
|         result.content["order"] = order
 | |
|     return result
 | |
| 
 | |
| 
 | |
| def _order(*events):
 | |
|     return sorted(events, key=_child_events_comparison_key)
 | |
| 
 | |
| 
 | |
| class TestSpaceSummarySort(unittest.TestCase):
 | |
|     def test_no_order_last(self):
 | |
|         """An event with no ordering is placed behind those with an ordering."""
 | |
|         ev1 = _create_event("!abc:test")
 | |
|         ev2 = _create_event("!xyz:test", "xyz")
 | |
| 
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|     def test_order(self):
 | |
|         """The ordering should be used."""
 | |
|         ev1 = _create_event("!abc:test", "xyz")
 | |
|         ev2 = _create_event("!xyz:test", "abc")
 | |
| 
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|     def test_order_room_id(self):
 | |
|         """Room ID is a tie-breaker for ordering."""
 | |
|         ev1 = _create_event("!abc:test", "abc")
 | |
|         ev2 = _create_event("!xyz:test", "abc")
 | |
| 
 | |
|         self.assertEqual([ev1, ev2], _order(ev1, ev2))
 | |
| 
 | |
|     def test_invalid_ordering_type(self):
 | |
|         """Invalid orderings are considered the same as missing."""
 | |
|         ev1 = _create_event("!abc:test", 1)
 | |
|         ev2 = _create_event("!xyz:test", "xyz")
 | |
| 
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|         ev1 = _create_event("!abc:test", {})
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|         ev1 = _create_event("!abc:test", [])
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|         ev1 = _create_event("!abc:test", True)
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|     def test_invalid_ordering_value(self):
 | |
|         """Invalid orderings are considered the same as missing."""
 | |
|         ev1 = _create_event("!abc:test", "foo\n")
 | |
|         ev2 = _create_event("!xyz:test", "xyz")
 | |
| 
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
|         ev1 = _create_event("!abc:test", "a" * 51)
 | |
|         self.assertEqual([ev2, ev1], _order(ev1, ev2))
 | |
| 
 | |
| 
 | |
| class SpaceSummaryTestCase(unittest.HomeserverTestCase):
 | |
|     servlets = [
 | |
|         admin.register_servlets_for_client_rest_resource,
 | |
|         room.register_servlets,
 | |
|         login.register_servlets,
 | |
|     ]
 | |
| 
 | |
|     def prepare(self, reactor, clock, hs: HomeServer):
 | |
|         self.hs = hs
 | |
|         self.handler = self.hs.get_space_summary_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, self.token)
 | |
| 
 | |
|     def _add_child(self, space_id: str, room_id: str, token: str) -> None:
 | |
|         """Add a child room to a space."""
 | |
|         self.helper.send_state(
 | |
|             space_id,
 | |
|             event_type=EventTypes.SpaceChild,
 | |
|             body={"via": [self.hs.hostname]},
 | |
|             tok=token,
 | |
|             state_key=room_id,
 | |
|         )
 | |
| 
 | |
|     def _assert_rooms(self, result: JsonDict, rooms: Iterable[str]) -> None:
 | |
|         """Assert that the expected room IDs are in the response."""
 | |
|         self.assertCountEqual([room.get("room_id") for room in result["rooms"]], rooms)
 | |
| 
 | |
|     def _assert_events(
 | |
|         self, result: JsonDict, events: Iterable[Tuple[str, str]]
 | |
|     ) -> None:
 | |
|         """Assert that the expected parent / child room IDs are in the response."""
 | |
|         self.assertCountEqual(
 | |
|             [
 | |
|                 (event.get("room_id"), event.get("state_key"))
 | |
|                 for event in result["events"]
 | |
|             ],
 | |
|             events,
 | |
|         )
 | |
| 
 | |
|     def test_simple_space(self):
 | |
|         """Test a simple space with a single room."""
 | |
|         result = self.get_success(self.handler.get_space_summary(self.user, self.space))
 | |
|         # The result should have the space and the room in it, along with a link
 | |
|         # from space -> room.
 | |
|         self._assert_rooms(result, [self.space, self.room])
 | |
|         self._assert_events(result, [(self.space, self.room)])
 | |
| 
 | |
|     def test_visibility(self):
 | |
|         """A user not in a space cannot inspect it."""
 | |
|         user2 = self.register_user("user2", "pass")
 | |
|         token2 = self.login("user2", "pass")
 | |
| 
 | |
|         # The user cannot see the space.
 | |
|         self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError)
 | |
| 
 | |
|         # If the space is made world-readable it should return a result.
 | |
|         self.helper.send_state(
 | |
|             self.space,
 | |
|             event_type=EventTypes.RoomHistoryVisibility,
 | |
|             body={"history_visibility": HistoryVisibility.WORLD_READABLE},
 | |
|             tok=self.token,
 | |
|         )
 | |
|         result = self.get_success(self.handler.get_space_summary(user2, self.space))
 | |
|         self._assert_rooms(result, [self.space, self.room])
 | |
|         self._assert_events(result, [(self.space, self.room)])
 | |
| 
 | |
|         # Make it not world-readable again and confirm it results in an error.
 | |
|         self.helper.send_state(
 | |
|             self.space,
 | |
|             event_type=EventTypes.RoomHistoryVisibility,
 | |
|             body={"history_visibility": HistoryVisibility.JOINED},
 | |
|             tok=self.token,
 | |
|         )
 | |
|         self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError)
 | |
| 
 | |
|         # Join the space and results should be returned.
 | |
|         self.helper.join(self.space, user2, tok=token2)
 | |
|         result = self.get_success(self.handler.get_space_summary(user2, self.space))
 | |
|         self._assert_rooms(result, [self.space, self.room])
 | |
|         self._assert_events(result, [(self.space, self.room)])
 | |
| 
 | |
|     def _create_room_with_join_rule(
 | |
|         self, join_rule: str, room_version: Optional[str] = None, **extra_content
 | |
|     ) -> str:
 | |
|         """Create a room with the given join rule and add it to the space."""
 | |
|         room_id = self.helper.create_room_as(
 | |
|             self.user,
 | |
|             room_version=room_version,
 | |
|             tok=self.token,
 | |
|             extra_content={
 | |
|                 "initial_state": [
 | |
|                     {
 | |
|                         "type": EventTypes.JoinRules,
 | |
|                         "state_key": "",
 | |
|                         "content": {
 | |
|                             "join_rule": join_rule,
 | |
|                             **extra_content,
 | |
|                         },
 | |
|                     }
 | |
|                 ]
 | |
|             },
 | |
|         )
 | |
|         self._add_child(self.space, room_id, self.token)
 | |
|         return room_id
 | |
| 
 | |
|     def test_filtering(self):
 | |
|         """
 | |
|         Rooms should be properly filtered to only include rooms the user has access to.
 | |
|         """
 | |
|         user2 = self.register_user("user2", "pass")
 | |
|         token2 = self.login("user2", "pass")
 | |
| 
 | |
|         # Create a few rooms which will have different properties.
 | |
|         public_room = self._create_room_with_join_rule(JoinRules.PUBLIC)
 | |
|         knock_room = self._create_room_with_join_rule(
 | |
|             JoinRules.KNOCK, room_version=RoomVersions.V7.identifier
 | |
|         )
 | |
|         not_invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
 | |
|         invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
 | |
|         self.helper.invite(invited_room, targ=user2, tok=self.token)
 | |
|         restricted_room = self._create_room_with_join_rule(
 | |
|             JoinRules.MSC3083_RESTRICTED,
 | |
|             room_version=RoomVersions.MSC3083.identifier,
 | |
|             allow=[],
 | |
|         )
 | |
|         restricted_accessible_room = self._create_room_with_join_rule(
 | |
|             JoinRules.MSC3083_RESTRICTED,
 | |
|             room_version=RoomVersions.MSC3083.identifier,
 | |
|             allow=[
 | |
|                 {
 | |
|                     "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP,
 | |
|                     "room_id": self.space,
 | |
|                     "via": [self.hs.hostname],
 | |
|                 }
 | |
|             ],
 | |
|         )
 | |
|         world_readable_room = self._create_room_with_join_rule(JoinRules.INVITE)
 | |
|         self.helper.send_state(
 | |
|             world_readable_room,
 | |
|             event_type=EventTypes.RoomHistoryVisibility,
 | |
|             body={"history_visibility": HistoryVisibility.WORLD_READABLE},
 | |
|             tok=self.token,
 | |
|         )
 | |
|         joined_room = self._create_room_with_join_rule(JoinRules.INVITE)
 | |
|         self.helper.invite(joined_room, targ=user2, tok=self.token)
 | |
|         self.helper.join(joined_room, user2, tok=token2)
 | |
| 
 | |
|         # Join the space.
 | |
|         self.helper.join(self.space, user2, tok=token2)
 | |
|         result = self.get_success(self.handler.get_space_summary(user2, self.space))
 | |
| 
 | |
|         self._assert_rooms(
 | |
|             result,
 | |
|             [
 | |
|                 self.space,
 | |
|                 self.room,
 | |
|                 public_room,
 | |
|                 knock_room,
 | |
|                 invited_room,
 | |
|                 restricted_accessible_room,
 | |
|                 world_readable_room,
 | |
|                 joined_room,
 | |
|             ],
 | |
|         )
 | |
|         self._assert_events(
 | |
|             result,
 | |
|             [
 | |
|                 (self.space, self.room),
 | |
|                 (self.space, public_room),
 | |
|                 (self.space, knock_room),
 | |
|                 (self.space, not_invited_room),
 | |
|                 (self.space, invited_room),
 | |
|                 (self.space, restricted_room),
 | |
|                 (self.space, restricted_accessible_room),
 | |
|                 (self.space, world_readable_room),
 | |
|                 (self.space, joined_room),
 | |
|             ],
 | |
|         )
 | |
| 
 | |
|     def test_complex_space(self):
 | |
|         """
 | |
|         Create a "complex" space to see how it handles things like loops and subspaces.
 | |
|         """
 | |
|         # Create an inaccessible room.
 | |
|         user2 = self.register_user("user2", "pass")
 | |
|         token2 = self.login("user2", "pass")
 | |
|         room2 = self.helper.create_room_as(user2, is_public=False, tok=token2)
 | |
|         # This is a bit odd as "user" is adding a room they don't know about, but
 | |
|         # it works for the tests.
 | |
|         self._add_child(self.space, room2, self.token)
 | |
| 
 | |
|         # Create a subspace under the space with an additional room in it.
 | |
|         subspace = self.helper.create_room_as(
 | |
|             self.user,
 | |
|             tok=self.token,
 | |
|             extra_content={
 | |
|                 "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
 | |
|             },
 | |
|         )
 | |
|         subroom = self.helper.create_room_as(self.user, tok=self.token)
 | |
|         self._add_child(self.space, subspace, token=self.token)
 | |
|         self._add_child(subspace, subroom, token=self.token)
 | |
|         # Also add the two rooms from the space into this subspace (causing loops).
 | |
|         self._add_child(subspace, self.room, token=self.token)
 | |
|         self._add_child(subspace, room2, self.token)
 | |
| 
 | |
|         result = self.get_success(self.handler.get_space_summary(self.user, self.space))
 | |
| 
 | |
|         # The result should include each room a single time and each link.
 | |
|         self._assert_rooms(result, [self.space, self.room, subspace, subroom])
 | |
|         self._assert_events(
 | |
|             result,
 | |
|             [
 | |
|                 (self.space, self.room),
 | |
|                 (self.space, room2),
 | |
|                 (self.space, subspace),
 | |
|                 (subspace, subroom),
 | |
|                 (subspace, self.room),
 | |
|                 (subspace, room2),
 | |
|             ],
 | |
|         )
 | |
| 
 | |
|     def test_fed_complex(self):
 | |
|         """
 | |
|         Return data over federation and ensure that it is handled properly.
 | |
|         """
 | |
|         fed_hostname = self.hs.hostname + "2"
 | |
|         subspace = "#subspace:" + fed_hostname
 | |
|         subroom = "#subroom:" + fed_hostname
 | |
| 
 | |
|         async def summarize_remote_room(
 | |
|             _self, room, suggested_only, max_children, exclude_rooms
 | |
|         ):
 | |
|             # Return some good data, and some bad data:
 | |
|             #
 | |
|             # * Event *back* to the root room.
 | |
|             # * Unrelated events / rooms
 | |
|             # * Multiple levels of events (in a not-useful order, e.g. grandchild
 | |
|             #   events before child events).
 | |
| 
 | |
|             # Note that these entries are brief, but should contain enough info.
 | |
|             return [
 | |
|                 _RoomEntry(
 | |
|                     subspace,
 | |
|                     {
 | |
|                         "room_id": subspace,
 | |
|                         "world_readable": True,
 | |
|                         "room_type": RoomTypes.SPACE,
 | |
|                     },
 | |
|                     [
 | |
|                         {
 | |
|                             "room_id": subspace,
 | |
|                             "state_key": subroom,
 | |
|                             "content": {"via": [fed_hostname]},
 | |
|                         }
 | |
|                     ],
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     subroom,
 | |
|                     {
 | |
|                         "room_id": subroom,
 | |
|                         "world_readable": True,
 | |
|                     },
 | |
|                 ),
 | |
|             ]
 | |
| 
 | |
|         # Add a room to the space which is on another server.
 | |
|         self._add_child(self.space, subspace, self.token)
 | |
| 
 | |
|         with mock.patch(
 | |
|             "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
 | |
|             new=summarize_remote_room,
 | |
|         ):
 | |
|             result = self.get_success(
 | |
|                 self.handler.get_space_summary(self.user, self.space)
 | |
|             )
 | |
| 
 | |
|         self._assert_rooms(result, [self.space, self.room, subspace, subroom])
 | |
|         self._assert_events(
 | |
|             result,
 | |
|             [
 | |
|                 (self.space, self.room),
 | |
|                 (self.space, subspace),
 | |
|                 (subspace, subroom),
 | |
|             ],
 | |
|         )
 | |
| 
 | |
|     def test_fed_filtering(self):
 | |
|         """
 | |
|         Rooms returned over federation should be properly filtered to only include
 | |
|         rooms the user has access to.
 | |
|         """
 | |
|         fed_hostname = self.hs.hostname + "2"
 | |
|         subspace = "#subspace:" + fed_hostname
 | |
| 
 | |
|         # Create a few rooms which will have different properties.
 | |
|         public_room = "#public:" + fed_hostname
 | |
|         knock_room = "#knock:" + fed_hostname
 | |
|         not_invited_room = "#not_invited:" + fed_hostname
 | |
|         invited_room = "#invited:" + fed_hostname
 | |
|         restricted_room = "#restricted:" + fed_hostname
 | |
|         restricted_accessible_room = "#restricted_accessible:" + fed_hostname
 | |
|         world_readable_room = "#world_readable:" + fed_hostname
 | |
|         joined_room = self.helper.create_room_as(self.user, tok=self.token)
 | |
| 
 | |
|         # Poke an invite over federation into the database.
 | |
|         fed_handler = self.hs.get_federation_handler()
 | |
|         event = make_event_from_dict(
 | |
|             {
 | |
|                 "room_id": invited_room,
 | |
|                 "event_id": "!abcd:" + fed_hostname,
 | |
|                 "type": EventTypes.Member,
 | |
|                 "sender": "@remote:" + fed_hostname,
 | |
|                 "state_key": self.user,
 | |
|                 "content": {"membership": Membership.INVITE},
 | |
|                 "prev_events": [],
 | |
|                 "auth_events": [],
 | |
|                 "depth": 1,
 | |
|                 "origin_server_ts": 1234,
 | |
|             }
 | |
|         )
 | |
|         self.get_success(
 | |
|             fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6)
 | |
|         )
 | |
| 
 | |
|         async def summarize_remote_room(
 | |
|             _self, room, suggested_only, max_children, exclude_rooms
 | |
|         ):
 | |
|             # Note that these entries are brief, but should contain enough info.
 | |
|             rooms = [
 | |
|                 _RoomEntry(
 | |
|                     public_room,
 | |
|                     {
 | |
|                         "room_id": public_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.PUBLIC,
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     knock_room,
 | |
|                     {
 | |
|                         "room_id": knock_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.KNOCK,
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     not_invited_room,
 | |
|                     {
 | |
|                         "room_id": not_invited_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.INVITE,
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     invited_room,
 | |
|                     {
 | |
|                         "room_id": invited_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.INVITE,
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     restricted_room,
 | |
|                     {
 | |
|                         "room_id": restricted_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.MSC3083_RESTRICTED,
 | |
|                         "allowed_spaces": [],
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     restricted_accessible_room,
 | |
|                     {
 | |
|                         "room_id": restricted_accessible_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.MSC3083_RESTRICTED,
 | |
|                         "allowed_spaces": [self.room],
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     world_readable_room,
 | |
|                     {
 | |
|                         "room_id": world_readable_room,
 | |
|                         "world_readable": True,
 | |
|                         "join_rules": JoinRules.INVITE,
 | |
|                     },
 | |
|                 ),
 | |
|                 _RoomEntry(
 | |
|                     joined_room,
 | |
|                     {
 | |
|                         "room_id": joined_room,
 | |
|                         "world_readable": False,
 | |
|                         "join_rules": JoinRules.INVITE,
 | |
|                     },
 | |
|                 ),
 | |
|             ]
 | |
| 
 | |
|             # Also include the subspace.
 | |
|             rooms.insert(
 | |
|                 0,
 | |
|                 _RoomEntry(
 | |
|                     subspace,
 | |
|                     {
 | |
|                         "room_id": subspace,
 | |
|                         "world_readable": True,
 | |
|                     },
 | |
|                     # Place each room in the sub-space.
 | |
|                     [
 | |
|                         {
 | |
|                             "room_id": subspace,
 | |
|                             "state_key": room.room_id,
 | |
|                             "content": {"via": [fed_hostname]},
 | |
|                         }
 | |
|                         for room in rooms
 | |
|                     ],
 | |
|                 ),
 | |
|             )
 | |
|             return rooms
 | |
| 
 | |
|         # Add a room to the space which is on another server.
 | |
|         self._add_child(self.space, subspace, self.token)
 | |
| 
 | |
|         with mock.patch(
 | |
|             "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
 | |
|             new=summarize_remote_room,
 | |
|         ):
 | |
|             result = self.get_success(
 | |
|                 self.handler.get_space_summary(self.user, self.space)
 | |
|             )
 | |
| 
 | |
|         self._assert_rooms(
 | |
|             result,
 | |
|             [
 | |
|                 self.space,
 | |
|                 self.room,
 | |
|                 subspace,
 | |
|                 public_room,
 | |
|                 knock_room,
 | |
|                 invited_room,
 | |
|                 restricted_accessible_room,
 | |
|                 world_readable_room,
 | |
|                 joined_room,
 | |
|             ],
 | |
|         )
 | |
|         self._assert_events(
 | |
|             result,
 | |
|             [
 | |
|                 (self.space, self.room),
 | |
|                 (self.space, subspace),
 | |
|                 (subspace, public_room),
 | |
|                 (subspace, knock_room),
 | |
|                 (subspace, not_invited_room),
 | |
|                 (subspace, invited_room),
 | |
|                 (subspace, restricted_room),
 | |
|                 (subspace, restricted_accessible_room),
 | |
|                 (subspace, world_readable_room),
 | |
|                 (subspace, joined_room),
 | |
|             ],
 | |
|         )
 |