test_space_summary.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. # Copyright 2021 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Any, Iterable, List, Optional, Tuple
  15. from unittest import mock
  16. from synapse.api.constants import (
  17. EventContentFields,
  18. EventTypes,
  19. HistoryVisibility,
  20. JoinRules,
  21. Membership,
  22. RestrictedJoinRuleTypes,
  23. RoomTypes,
  24. )
  25. from synapse.api.errors import AuthError, SynapseError
  26. from synapse.api.room_versions import RoomVersions
  27. from synapse.events import make_event_from_dict
  28. from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry
  29. from synapse.rest import admin
  30. from synapse.rest.client.v1 import login, room
  31. from synapse.server import HomeServer
  32. from synapse.types import JsonDict, UserID
  33. from tests import unittest
  34. def _create_event(room_id: str, order: Optional[Any] = None):
  35. result = mock.Mock()
  36. result.room_id = room_id
  37. result.content = {}
  38. if order is not None:
  39. result.content["order"] = order
  40. return result
  41. def _order(*events):
  42. return sorted(events, key=_child_events_comparison_key)
  43. class TestSpaceSummarySort(unittest.TestCase):
  44. def test_no_order_last(self):
  45. """An event with no ordering is placed behind those with an ordering."""
  46. ev1 = _create_event("!abc:test")
  47. ev2 = _create_event("!xyz:test", "xyz")
  48. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  49. def test_order(self):
  50. """The ordering should be used."""
  51. ev1 = _create_event("!abc:test", "xyz")
  52. ev2 = _create_event("!xyz:test", "abc")
  53. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  54. def test_order_room_id(self):
  55. """Room ID is a tie-breaker for ordering."""
  56. ev1 = _create_event("!abc:test", "abc")
  57. ev2 = _create_event("!xyz:test", "abc")
  58. self.assertEqual([ev1, ev2], _order(ev1, ev2))
  59. def test_invalid_ordering_type(self):
  60. """Invalid orderings are considered the same as missing."""
  61. ev1 = _create_event("!abc:test", 1)
  62. ev2 = _create_event("!xyz:test", "xyz")
  63. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  64. ev1 = _create_event("!abc:test", {})
  65. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  66. ev1 = _create_event("!abc:test", [])
  67. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  68. ev1 = _create_event("!abc:test", True)
  69. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  70. def test_invalid_ordering_value(self):
  71. """Invalid orderings are considered the same as missing."""
  72. ev1 = _create_event("!abc:test", "foo\n")
  73. ev2 = _create_event("!xyz:test", "xyz")
  74. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  75. ev1 = _create_event("!abc:test", "a" * 51)
  76. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  77. class SpaceSummaryTestCase(unittest.HomeserverTestCase):
  78. servlets = [
  79. admin.register_servlets_for_client_rest_resource,
  80. room.register_servlets,
  81. login.register_servlets,
  82. ]
  83. def prepare(self, reactor, clock, hs: HomeServer):
  84. self.hs = hs
  85. self.handler = self.hs.get_space_summary_handler()
  86. # Create a user.
  87. self.user = self.register_user("user", "pass")
  88. self.token = self.login("user", "pass")
  89. # Create a space and a child room.
  90. self.space = self.helper.create_room_as(
  91. self.user,
  92. tok=self.token,
  93. extra_content={
  94. "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
  95. },
  96. )
  97. self.room = self.helper.create_room_as(self.user, tok=self.token)
  98. self._add_child(self.space, self.room, self.token)
  99. def _add_child(
  100. self, space_id: str, room_id: str, token: str, order: Optional[str] = None
  101. ) -> None:
  102. """Add a child room to a space."""
  103. content: JsonDict = {"via": [self.hs.hostname]}
  104. if order is not None:
  105. content["order"] = order
  106. self.helper.send_state(
  107. space_id,
  108. event_type=EventTypes.SpaceChild,
  109. body=content,
  110. tok=token,
  111. state_key=room_id,
  112. )
  113. def _assert_rooms(
  114. self, result: JsonDict, rooms_and_children: Iterable[Tuple[str, Iterable[str]]]
  115. ) -> None:
  116. """
  117. Assert that the expected room IDs and events are in the response.
  118. Args:
  119. result: The result from the API call.
  120. rooms_and_children: An iterable of tuples where each tuple is:
  121. The expected room ID.
  122. The expected IDs of any children rooms.
  123. """
  124. room_ids = []
  125. children_ids = []
  126. for room_id, children in rooms_and_children:
  127. room_ids.append(room_id)
  128. if children:
  129. children_ids.extend([(room_id, child_id) for child_id in children])
  130. self.assertCountEqual(
  131. [room.get("room_id") for room in result["rooms"]], room_ids
  132. )
  133. self.assertCountEqual(
  134. [
  135. (event.get("room_id"), event.get("state_key"))
  136. for event in result["events"]
  137. ],
  138. children_ids,
  139. )
  140. def _assert_hierarchy(
  141. self, result: JsonDict, rooms_and_children: Iterable[Tuple[str, Iterable[str]]]
  142. ) -> None:
  143. """
  144. Assert that the expected room IDs are in the response.
  145. Args:
  146. result: The result from the API call.
  147. rooms_and_children: An iterable of tuples where each tuple is:
  148. The expected room ID.
  149. The expected IDs of any children rooms.
  150. """
  151. result_room_ids = []
  152. result_children_ids = []
  153. for result_room in result["rooms"]:
  154. result_room_ids.append(result_room["room_id"])
  155. result_children_ids.append(
  156. [
  157. (cs["room_id"], cs["state_key"])
  158. for cs in result_room.get("children_state")
  159. ]
  160. )
  161. room_ids = []
  162. children_ids = []
  163. for room_id, children in rooms_and_children:
  164. room_ids.append(room_id)
  165. children_ids.append([(room_id, child_id) for child_id in children])
  166. # Note that order matters.
  167. self.assertEqual(result_room_ids, room_ids)
  168. self.assertEqual(result_children_ids, children_ids)
  169. def _poke_fed_invite(self, room_id: str, from_user: str) -> None:
  170. """
  171. Creates a invite (as if received over federation) for the room from the
  172. given hostname.
  173. Args:
  174. room_id: The room ID to issue an invite for.
  175. fed_hostname: The user to invite from.
  176. """
  177. # Poke an invite over federation into the database.
  178. fed_handler = self.hs.get_federation_handler()
  179. fed_hostname = UserID.from_string(from_user).domain
  180. event = make_event_from_dict(
  181. {
  182. "room_id": room_id,
  183. "event_id": "!abcd:" + fed_hostname,
  184. "type": EventTypes.Member,
  185. "sender": from_user,
  186. "state_key": self.user,
  187. "content": {"membership": Membership.INVITE},
  188. "prev_events": [],
  189. "auth_events": [],
  190. "depth": 1,
  191. "origin_server_ts": 1234,
  192. }
  193. )
  194. self.get_success(
  195. fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6)
  196. )
  197. def test_simple_space(self):
  198. """Test a simple space with a single room."""
  199. result = self.get_success(self.handler.get_space_summary(self.user, self.space))
  200. # The result should have the space and the room in it, along with a link
  201. # from space -> room.
  202. expected = [(self.space, [self.room]), (self.room, ())]
  203. self._assert_rooms(result, expected)
  204. result = self.get_success(
  205. self.handler.get_room_hierarchy(self.user, self.space)
  206. )
  207. self._assert_hierarchy(result, expected)
  208. def test_visibility(self):
  209. """A user not in a space cannot inspect it."""
  210. user2 = self.register_user("user2", "pass")
  211. token2 = self.login("user2", "pass")
  212. # The user can see the space since it is publicly joinable.
  213. result = self.get_success(self.handler.get_space_summary(user2, self.space))
  214. expected = [(self.space, [self.room]), (self.room, ())]
  215. self._assert_rooms(result, expected)
  216. result = self.get_success(self.handler.get_room_hierarchy(user2, self.space))
  217. self._assert_hierarchy(result, expected)
  218. # If the space is made invite-only, it should no longer be viewable.
  219. self.helper.send_state(
  220. self.space,
  221. event_type=EventTypes.JoinRules,
  222. body={"join_rule": JoinRules.INVITE},
  223. tok=self.token,
  224. )
  225. self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError)
  226. self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError)
  227. # If the space is made world-readable it should return a result.
  228. self.helper.send_state(
  229. self.space,
  230. event_type=EventTypes.RoomHistoryVisibility,
  231. body={"history_visibility": HistoryVisibility.WORLD_READABLE},
  232. tok=self.token,
  233. )
  234. result = self.get_success(self.handler.get_space_summary(user2, self.space))
  235. self._assert_rooms(result, expected)
  236. result = self.get_success(self.handler.get_room_hierarchy(user2, self.space))
  237. self._assert_hierarchy(result, expected)
  238. # Make it not world-readable again and confirm it results in an error.
  239. self.helper.send_state(
  240. self.space,
  241. event_type=EventTypes.RoomHistoryVisibility,
  242. body={"history_visibility": HistoryVisibility.JOINED},
  243. tok=self.token,
  244. )
  245. self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError)
  246. self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError)
  247. # Join the space and results should be returned.
  248. self.helper.invite(self.space, targ=user2, tok=self.token)
  249. self.helper.join(self.space, user2, tok=token2)
  250. result = self.get_success(self.handler.get_space_summary(user2, self.space))
  251. self._assert_rooms(result, expected)
  252. result = self.get_success(self.handler.get_room_hierarchy(user2, self.space))
  253. self._assert_hierarchy(result, expected)
  254. # Attempting to view an unknown room returns the same error.
  255. self.get_failure(
  256. self.handler.get_space_summary(user2, "#not-a-space:" + self.hs.hostname),
  257. AuthError,
  258. )
  259. self.get_failure(
  260. self.handler.get_room_hierarchy(user2, "#not-a-space:" + self.hs.hostname),
  261. AuthError,
  262. )
  263. def _create_room_with_join_rule(
  264. self, join_rule: str, room_version: Optional[str] = None, **extra_content
  265. ) -> str:
  266. """Create a room with the given join rule and add it to the space."""
  267. room_id = self.helper.create_room_as(
  268. self.user,
  269. room_version=room_version,
  270. tok=self.token,
  271. extra_content={
  272. "initial_state": [
  273. {
  274. "type": EventTypes.JoinRules,
  275. "state_key": "",
  276. "content": {
  277. "join_rule": join_rule,
  278. **extra_content,
  279. },
  280. }
  281. ]
  282. },
  283. )
  284. self._add_child(self.space, room_id, self.token)
  285. return room_id
  286. def test_filtering(self):
  287. """
  288. Rooms should be properly filtered to only include rooms the user has access to.
  289. """
  290. user2 = self.register_user("user2", "pass")
  291. token2 = self.login("user2", "pass")
  292. # Create a few rooms which will have different properties.
  293. public_room = self._create_room_with_join_rule(JoinRules.PUBLIC)
  294. knock_room = self._create_room_with_join_rule(
  295. JoinRules.KNOCK, room_version=RoomVersions.V7.identifier
  296. )
  297. not_invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
  298. invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
  299. self.helper.invite(invited_room, targ=user2, tok=self.token)
  300. restricted_room = self._create_room_with_join_rule(
  301. JoinRules.RESTRICTED,
  302. room_version=RoomVersions.V8.identifier,
  303. allow=[],
  304. )
  305. restricted_accessible_room = self._create_room_with_join_rule(
  306. JoinRules.RESTRICTED,
  307. room_version=RoomVersions.V8.identifier,
  308. allow=[
  309. {
  310. "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP,
  311. "room_id": self.space,
  312. "via": [self.hs.hostname],
  313. }
  314. ],
  315. )
  316. world_readable_room = self._create_room_with_join_rule(JoinRules.INVITE)
  317. self.helper.send_state(
  318. world_readable_room,
  319. event_type=EventTypes.RoomHistoryVisibility,
  320. body={"history_visibility": HistoryVisibility.WORLD_READABLE},
  321. tok=self.token,
  322. )
  323. joined_room = self._create_room_with_join_rule(JoinRules.INVITE)
  324. self.helper.invite(joined_room, targ=user2, tok=self.token)
  325. self.helper.join(joined_room, user2, tok=token2)
  326. # Join the space.
  327. self.helper.join(self.space, user2, tok=token2)
  328. result = self.get_success(self.handler.get_space_summary(user2, self.space))
  329. expected = [
  330. (
  331. self.space,
  332. [
  333. self.room,
  334. public_room,
  335. knock_room,
  336. not_invited_room,
  337. invited_room,
  338. restricted_room,
  339. restricted_accessible_room,
  340. world_readable_room,
  341. joined_room,
  342. ],
  343. ),
  344. (self.room, ()),
  345. (public_room, ()),
  346. (knock_room, ()),
  347. (invited_room, ()),
  348. (restricted_accessible_room, ()),
  349. (world_readable_room, ()),
  350. (joined_room, ()),
  351. ]
  352. self._assert_rooms(result, expected)
  353. result = self.get_success(self.handler.get_room_hierarchy(user2, self.space))
  354. self._assert_hierarchy(result, expected)
  355. def test_complex_space(self):
  356. """
  357. Create a "complex" space to see how it handles things like loops and subspaces.
  358. """
  359. # Create an inaccessible room.
  360. user2 = self.register_user("user2", "pass")
  361. token2 = self.login("user2", "pass")
  362. room2 = self.helper.create_room_as(user2, is_public=False, tok=token2)
  363. # This is a bit odd as "user" is adding a room they don't know about, but
  364. # it works for the tests.
  365. self._add_child(self.space, room2, self.token)
  366. # Create a subspace under the space with an additional room in it.
  367. subspace = self.helper.create_room_as(
  368. self.user,
  369. tok=self.token,
  370. extra_content={
  371. "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
  372. },
  373. )
  374. subroom = self.helper.create_room_as(self.user, tok=self.token)
  375. self._add_child(self.space, subspace, token=self.token)
  376. self._add_child(subspace, subroom, token=self.token)
  377. # Also add the two rooms from the space into this subspace (causing loops).
  378. self._add_child(subspace, self.room, token=self.token)
  379. self._add_child(subspace, room2, self.token)
  380. result = self.get_success(self.handler.get_space_summary(self.user, self.space))
  381. # The result should include each room a single time and each link.
  382. expected = [
  383. (self.space, [self.room, room2, subspace]),
  384. (self.room, ()),
  385. (subspace, [subroom, self.room, room2]),
  386. (subroom, ()),
  387. ]
  388. self._assert_rooms(result, expected)
  389. result = self.get_success(
  390. self.handler.get_room_hierarchy(self.user, self.space)
  391. )
  392. self._assert_hierarchy(result, expected)
  393. def test_pagination(self):
  394. """Test simple pagination works."""
  395. room_ids = []
  396. for i in range(1, 10):
  397. room = self.helper.create_room_as(self.user, tok=self.token)
  398. self._add_child(self.space, room, self.token, order=str(i))
  399. room_ids.append(room)
  400. # The room created initially doesn't have an order, so comes last.
  401. room_ids.append(self.room)
  402. result = self.get_success(
  403. self.handler.get_room_hierarchy(self.user, self.space, limit=7)
  404. )
  405. # The result should have the space and all of the links, plus some of the
  406. # rooms and a pagination token.
  407. expected: List[Tuple[str, Iterable[str]]] = [(self.space, room_ids)]
  408. expected += [(room_id, ()) for room_id in room_ids[:6]]
  409. self._assert_hierarchy(result, expected)
  410. self.assertIn("next_batch", result)
  411. # Check the next page.
  412. result = self.get_success(
  413. self.handler.get_room_hierarchy(
  414. self.user, self.space, limit=5, from_token=result["next_batch"]
  415. )
  416. )
  417. # The result should have the space and the room in it, along with a link
  418. # from space -> room.
  419. expected = [(room_id, ()) for room_id in room_ids[6:]]
  420. self._assert_hierarchy(result, expected)
  421. self.assertNotIn("next_batch", result)
  422. def test_invalid_pagination_token(self):
  423. """An invalid pagination token, or changing other parameters, shoudl be rejected."""
  424. room_ids = []
  425. for i in range(1, 10):
  426. room = self.helper.create_room_as(self.user, tok=self.token)
  427. self._add_child(self.space, room, self.token, order=str(i))
  428. room_ids.append(room)
  429. # The room created initially doesn't have an order, so comes last.
  430. room_ids.append(self.room)
  431. result = self.get_success(
  432. self.handler.get_room_hierarchy(self.user, self.space, limit=7)
  433. )
  434. self.assertIn("next_batch", result)
  435. # Changing the room ID, suggested-only, or max-depth causes an error.
  436. self.get_failure(
  437. self.handler.get_room_hierarchy(
  438. self.user, self.room, from_token=result["next_batch"]
  439. ),
  440. SynapseError,
  441. )
  442. self.get_failure(
  443. self.handler.get_room_hierarchy(
  444. self.user,
  445. self.space,
  446. suggested_only=True,
  447. from_token=result["next_batch"],
  448. ),
  449. SynapseError,
  450. )
  451. self.get_failure(
  452. self.handler.get_room_hierarchy(
  453. self.user, self.space, max_depth=0, from_token=result["next_batch"]
  454. ),
  455. SynapseError,
  456. )
  457. # An invalid token is ignored.
  458. self.get_failure(
  459. self.handler.get_room_hierarchy(self.user, self.space, from_token="foo"),
  460. SynapseError,
  461. )
  462. def test_max_depth(self):
  463. """Create a deep tree to test the max depth against."""
  464. spaces = [self.space]
  465. rooms = [self.room]
  466. for _ in range(5):
  467. spaces.append(
  468. self.helper.create_room_as(
  469. self.user,
  470. tok=self.token,
  471. extra_content={
  472. "creation_content": {
  473. EventContentFields.ROOM_TYPE: RoomTypes.SPACE
  474. }
  475. },
  476. )
  477. )
  478. self._add_child(spaces[-2], spaces[-1], self.token)
  479. rooms.append(self.helper.create_room_as(self.user, tok=self.token))
  480. self._add_child(spaces[-1], rooms[-1], self.token)
  481. # Test just the space itself.
  482. result = self.get_success(
  483. self.handler.get_room_hierarchy(self.user, self.space, max_depth=0)
  484. )
  485. expected: List[Tuple[str, Iterable[str]]] = [(spaces[0], [rooms[0], spaces[1]])]
  486. self._assert_hierarchy(result, expected)
  487. # A single additional layer.
  488. result = self.get_success(
  489. self.handler.get_room_hierarchy(self.user, self.space, max_depth=1)
  490. )
  491. expected += [
  492. (rooms[0], ()),
  493. (spaces[1], [rooms[1], spaces[2]]),
  494. ]
  495. self._assert_hierarchy(result, expected)
  496. # A few layers.
  497. result = self.get_success(
  498. self.handler.get_room_hierarchy(self.user, self.space, max_depth=3)
  499. )
  500. expected += [
  501. (rooms[1], ()),
  502. (spaces[2], [rooms[2], spaces[3]]),
  503. (rooms[2], ()),
  504. (spaces[3], [rooms[3], spaces[4]]),
  505. ]
  506. self._assert_hierarchy(result, expected)
  507. def test_fed_complex(self):
  508. """
  509. Return data over federation and ensure that it is handled properly.
  510. """
  511. fed_hostname = self.hs.hostname + "2"
  512. subspace = "#subspace:" + fed_hostname
  513. subroom = "#subroom:" + fed_hostname
  514. # Generate some good data, and some bad data:
  515. #
  516. # * Event *back* to the root room.
  517. # * Unrelated events / rooms
  518. # * Multiple levels of events (in a not-useful order, e.g. grandchild
  519. # events before child events).
  520. # Note that these entries are brief, but should contain enough info.
  521. requested_room_entry = _RoomEntry(
  522. subspace,
  523. {
  524. "room_id": subspace,
  525. "world_readable": True,
  526. "room_type": RoomTypes.SPACE,
  527. },
  528. [
  529. {
  530. "type": EventTypes.SpaceChild,
  531. "room_id": subspace,
  532. "state_key": subroom,
  533. "content": {"via": [fed_hostname]},
  534. }
  535. ],
  536. )
  537. child_room = {
  538. "room_id": subroom,
  539. "world_readable": True,
  540. }
  541. async def summarize_remote_room(
  542. _self, room, suggested_only, max_children, exclude_rooms
  543. ):
  544. return [
  545. requested_room_entry,
  546. _RoomEntry(
  547. subroom,
  548. {
  549. "room_id": subroom,
  550. "world_readable": True,
  551. },
  552. ),
  553. ]
  554. async def summarize_remote_room_hiearchy(_self, room, suggested_only):
  555. return requested_room_entry, {subroom: child_room}, set()
  556. # Add a room to the space which is on another server.
  557. self._add_child(self.space, subspace, self.token)
  558. with mock.patch(
  559. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
  560. new=summarize_remote_room,
  561. ):
  562. result = self.get_success(
  563. self.handler.get_space_summary(self.user, self.space)
  564. )
  565. expected = [
  566. (self.space, [self.room, subspace]),
  567. (self.room, ()),
  568. (subspace, [subroom]),
  569. (subroom, ()),
  570. ]
  571. self._assert_rooms(result, expected)
  572. with mock.patch(
  573. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
  574. new=summarize_remote_room_hiearchy,
  575. ):
  576. result = self.get_success(
  577. self.handler.get_room_hierarchy(self.user, self.space)
  578. )
  579. self._assert_hierarchy(result, expected)
  580. def test_fed_filtering(self):
  581. """
  582. Rooms returned over federation should be properly filtered to only include
  583. rooms the user has access to.
  584. """
  585. fed_hostname = self.hs.hostname + "2"
  586. subspace = "#subspace:" + fed_hostname
  587. # Create a few rooms which will have different properties.
  588. public_room = "#public:" + fed_hostname
  589. knock_room = "#knock:" + fed_hostname
  590. not_invited_room = "#not_invited:" + fed_hostname
  591. invited_room = "#invited:" + fed_hostname
  592. restricted_room = "#restricted:" + fed_hostname
  593. restricted_accessible_room = "#restricted_accessible:" + fed_hostname
  594. world_readable_room = "#world_readable:" + fed_hostname
  595. joined_room = self.helper.create_room_as(self.user, tok=self.token)
  596. # Poke an invite over federation into the database.
  597. self._poke_fed_invite(invited_room, "@remote:" + fed_hostname)
  598. # Note that these entries are brief, but should contain enough info.
  599. children_rooms = (
  600. (
  601. public_room,
  602. {
  603. "room_id": public_room,
  604. "world_readable": False,
  605. "join_rules": JoinRules.PUBLIC,
  606. },
  607. ),
  608. (
  609. knock_room,
  610. {
  611. "room_id": knock_room,
  612. "world_readable": False,
  613. "join_rules": JoinRules.KNOCK,
  614. },
  615. ),
  616. (
  617. not_invited_room,
  618. {
  619. "room_id": not_invited_room,
  620. "world_readable": False,
  621. "join_rules": JoinRules.INVITE,
  622. },
  623. ),
  624. (
  625. invited_room,
  626. {
  627. "room_id": invited_room,
  628. "world_readable": False,
  629. "join_rules": JoinRules.INVITE,
  630. },
  631. ),
  632. (
  633. restricted_room,
  634. {
  635. "room_id": restricted_room,
  636. "world_readable": False,
  637. "join_rules": JoinRules.RESTRICTED,
  638. "allowed_spaces": [],
  639. },
  640. ),
  641. (
  642. restricted_accessible_room,
  643. {
  644. "room_id": restricted_accessible_room,
  645. "world_readable": False,
  646. "join_rules": JoinRules.RESTRICTED,
  647. "allowed_spaces": [self.room],
  648. },
  649. ),
  650. (
  651. world_readable_room,
  652. {
  653. "room_id": world_readable_room,
  654. "world_readable": True,
  655. "join_rules": JoinRules.INVITE,
  656. },
  657. ),
  658. (
  659. joined_room,
  660. {
  661. "room_id": joined_room,
  662. "world_readable": False,
  663. "join_rules": JoinRules.INVITE,
  664. },
  665. ),
  666. )
  667. subspace_room_entry = _RoomEntry(
  668. subspace,
  669. {
  670. "room_id": subspace,
  671. "world_readable": True,
  672. },
  673. # Place each room in the sub-space.
  674. [
  675. {
  676. "type": EventTypes.SpaceChild,
  677. "room_id": subspace,
  678. "state_key": room_id,
  679. "content": {"via": [fed_hostname]},
  680. }
  681. for room_id, _ in children_rooms
  682. ],
  683. )
  684. async def summarize_remote_room(
  685. _self, room, suggested_only, max_children, exclude_rooms
  686. ):
  687. return [subspace_room_entry] + [
  688. # A copy is made of the room data since the allowed_spaces key
  689. # is removed.
  690. _RoomEntry(child_room[0], dict(child_room[1]))
  691. for child_room in children_rooms
  692. ]
  693. async def summarize_remote_room_hiearchy(_self, room, suggested_only):
  694. return subspace_room_entry, dict(children_rooms), set()
  695. # Add a room to the space which is on another server.
  696. self._add_child(self.space, subspace, self.token)
  697. with mock.patch(
  698. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
  699. new=summarize_remote_room,
  700. ):
  701. result = self.get_success(
  702. self.handler.get_space_summary(self.user, self.space)
  703. )
  704. expected = [
  705. (self.space, [self.room, subspace]),
  706. (self.room, ()),
  707. (
  708. subspace,
  709. [
  710. public_room,
  711. knock_room,
  712. not_invited_room,
  713. invited_room,
  714. restricted_room,
  715. restricted_accessible_room,
  716. world_readable_room,
  717. joined_room,
  718. ],
  719. ),
  720. (public_room, ()),
  721. (knock_room, ()),
  722. (invited_room, ()),
  723. (restricted_accessible_room, ()),
  724. (world_readable_room, ()),
  725. (joined_room, ()),
  726. ]
  727. self._assert_rooms(result, expected)
  728. with mock.patch(
  729. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
  730. new=summarize_remote_room_hiearchy,
  731. ):
  732. result = self.get_success(
  733. self.handler.get_room_hierarchy(self.user, self.space)
  734. )
  735. self._assert_hierarchy(result, expected)
  736. def test_fed_invited(self):
  737. """
  738. A room which the user was invited to should be included in the response.
  739. This differs from test_fed_filtering in that the room itself is being
  740. queried over federation, instead of it being included as a sub-room of
  741. a space in the response.
  742. """
  743. fed_hostname = self.hs.hostname + "2"
  744. fed_room = "#subroom:" + fed_hostname
  745. # Poke an invite over federation into the database.
  746. self._poke_fed_invite(fed_room, "@remote:" + fed_hostname)
  747. fed_room_entry = _RoomEntry(
  748. fed_room,
  749. {
  750. "room_id": fed_room,
  751. "world_readable": False,
  752. "join_rules": JoinRules.INVITE,
  753. },
  754. )
  755. async def summarize_remote_room(
  756. _self, room, suggested_only, max_children, exclude_rooms
  757. ):
  758. return [fed_room_entry]
  759. async def summarize_remote_room_hiearchy(_self, room, suggested_only):
  760. return fed_room_entry, {}, set()
  761. # Add a room to the space which is on another server.
  762. self._add_child(self.space, fed_room, self.token)
  763. with mock.patch(
  764. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
  765. new=summarize_remote_room,
  766. ):
  767. result = self.get_success(
  768. self.handler.get_space_summary(self.user, self.space)
  769. )
  770. expected = [
  771. (self.space, [self.room, fed_room]),
  772. (self.room, ()),
  773. (fed_room, ()),
  774. ]
  775. self._assert_rooms(result, expected)
  776. with mock.patch(
  777. "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
  778. new=summarize_remote_room_hiearchy,
  779. ):
  780. result = self.get_success(
  781. self.handler.get_room_hierarchy(self.user, self.space)
  782. )
  783. self._assert_hierarchy(result, expected)