test_room_summary.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088
  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 twisted.internet.defer import ensureDeferred
  17. from synapse.api.constants import (
  18. EventContentFields,
  19. EventTypes,
  20. HistoryVisibility,
  21. JoinRules,
  22. Membership,
  23. RestrictedJoinRuleTypes,
  24. RoomTypes,
  25. )
  26. from synapse.api.errors import AuthError, NotFoundError, SynapseError
  27. from synapse.api.room_versions import RoomVersions
  28. from synapse.events import make_event_from_dict
  29. from synapse.federation.transport.client import TransportLayerClient
  30. from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry
  31. from synapse.rest import admin
  32. from synapse.rest.client import login, room
  33. from synapse.server import HomeServer
  34. from synapse.types import JsonDict, UserID, create_requester
  35. from tests import unittest
  36. def _create_event(room_id: str, order: Optional[Any] = None, origin_server_ts: int = 0):
  37. result = mock.Mock(name=room_id)
  38. result.room_id = room_id
  39. result.content = {}
  40. result.origin_server_ts = origin_server_ts
  41. if order is not None:
  42. result.content["order"] = order
  43. return result
  44. def _order(*events):
  45. return sorted(events, key=_child_events_comparison_key)
  46. class TestSpaceSummarySort(unittest.TestCase):
  47. def test_no_order_last(self):
  48. """An event with no ordering is placed behind those with an ordering."""
  49. ev1 = _create_event("!abc:test")
  50. ev2 = _create_event("!xyz:test", "xyz")
  51. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  52. def test_order(self):
  53. """The ordering should be used."""
  54. ev1 = _create_event("!abc:test", "xyz")
  55. ev2 = _create_event("!xyz:test", "abc")
  56. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  57. def test_order_origin_server_ts(self):
  58. """Origin server is a tie-breaker for ordering."""
  59. ev1 = _create_event("!abc:test", origin_server_ts=10)
  60. ev2 = _create_event("!xyz:test", origin_server_ts=30)
  61. self.assertEqual([ev1, ev2], _order(ev1, ev2))
  62. def test_order_room_id(self):
  63. """Room ID is a final tie-breaker for ordering."""
  64. ev1 = _create_event("!abc:test")
  65. ev2 = _create_event("!xyz:test")
  66. self.assertEqual([ev1, ev2], _order(ev1, ev2))
  67. def test_invalid_ordering_type(self):
  68. """Invalid orderings are considered the same as missing."""
  69. ev1 = _create_event("!abc:test", 1)
  70. ev2 = _create_event("!xyz:test", "xyz")
  71. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  72. ev1 = _create_event("!abc:test", {})
  73. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  74. ev1 = _create_event("!abc:test", [])
  75. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  76. ev1 = _create_event("!abc:test", True)
  77. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  78. def test_invalid_ordering_value(self):
  79. """Invalid orderings are considered the same as missing."""
  80. ev1 = _create_event("!abc:test", "foo\n")
  81. ev2 = _create_event("!xyz:test", "xyz")
  82. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  83. ev1 = _create_event("!abc:test", "a" * 51)
  84. self.assertEqual([ev2, ev1], _order(ev1, ev2))
  85. class SpaceSummaryTestCase(unittest.HomeserverTestCase):
  86. servlets = [
  87. admin.register_servlets_for_client_rest_resource,
  88. room.register_servlets,
  89. login.register_servlets,
  90. ]
  91. def prepare(self, reactor, clock, hs: HomeServer):
  92. self.hs = hs
  93. self.handler = self.hs.get_room_summary_handler()
  94. # Create a user.
  95. self.user = self.register_user("user", "pass")
  96. self.token = self.login("user", "pass")
  97. # Create a space and a child room.
  98. self.space = self.helper.create_room_as(
  99. self.user,
  100. tok=self.token,
  101. extra_content={
  102. "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
  103. },
  104. )
  105. self.room = self.helper.create_room_as(self.user, tok=self.token)
  106. self._add_child(self.space, self.room, self.token)
  107. def _add_child(
  108. self,
  109. space_id: str,
  110. room_id: str,
  111. token: str,
  112. order: Optional[str] = None,
  113. via: Optional[List[str]] = None,
  114. ) -> None:
  115. """Add a child room to a space."""
  116. if via is None:
  117. via = [self.hs.hostname]
  118. content: JsonDict = {"via": via}
  119. if order is not None:
  120. content["order"] = order
  121. self.helper.send_state(
  122. space_id,
  123. event_type=EventTypes.SpaceChild,
  124. body=content,
  125. tok=token,
  126. state_key=room_id,
  127. )
  128. def _assert_hierarchy(
  129. self, result: JsonDict, rooms_and_children: Iterable[Tuple[str, Iterable[str]]]
  130. ) -> None:
  131. """
  132. Assert that the expected room IDs are in the response.
  133. Args:
  134. result: The result from the API call.
  135. rooms_and_children: An iterable of tuples where each tuple is:
  136. The expected room ID.
  137. The expected IDs of any children rooms.
  138. """
  139. result_room_ids = []
  140. result_children_ids = []
  141. for result_room in result["rooms"]:
  142. result_room_ids.append(result_room["room_id"])
  143. result_children_ids.append(
  144. [
  145. (cs["room_id"], cs["state_key"])
  146. for cs in result_room.get("children_state")
  147. ]
  148. )
  149. room_ids = []
  150. children_ids = []
  151. for room_id, children in rooms_and_children:
  152. room_ids.append(room_id)
  153. children_ids.append([(room_id, child_id) for child_id in children])
  154. # Note that order matters.
  155. self.assertEqual(result_room_ids, room_ids)
  156. self.assertEqual(result_children_ids, children_ids)
  157. def _poke_fed_invite(self, room_id: str, from_user: str) -> None:
  158. """
  159. Creates a invite (as if received over federation) for the room from the
  160. given hostname.
  161. Args:
  162. room_id: The room ID to issue an invite for.
  163. fed_hostname: The user to invite from.
  164. """
  165. # Poke an invite over federation into the database.
  166. fed_handler = self.hs.get_federation_handler()
  167. fed_hostname = UserID.from_string(from_user).domain
  168. event = make_event_from_dict(
  169. {
  170. "room_id": room_id,
  171. "event_id": "!abcd:" + fed_hostname,
  172. "type": EventTypes.Member,
  173. "sender": from_user,
  174. "state_key": self.user,
  175. "content": {"membership": Membership.INVITE},
  176. "prev_events": [],
  177. "auth_events": [],
  178. "depth": 1,
  179. "origin_server_ts": 1234,
  180. }
  181. )
  182. self.get_success(
  183. fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6)
  184. )
  185. def test_simple_space(self):
  186. """Test a simple space with a single room."""
  187. # The result should have the space and the room in it, along with a link
  188. # from space -> room.
  189. expected = [(self.space, [self.room]), (self.room, ())]
  190. result = self.get_success(
  191. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  192. )
  193. self._assert_hierarchy(result, expected)
  194. def test_large_space(self):
  195. """Test a space with a large number of rooms."""
  196. rooms = [self.room]
  197. # Make at least 51 rooms that are part of the space.
  198. for _ in range(55):
  199. room = self.helper.create_room_as(self.user, tok=self.token)
  200. self._add_child(self.space, room, self.token)
  201. rooms.append(room)
  202. # The result should have the space and the rooms in it, along with the links
  203. # from space -> room.
  204. expected = [(self.space, rooms)] + [(room, []) for room in rooms]
  205. # Make two requests to fully paginate the results.
  206. result = self.get_success(
  207. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  208. )
  209. result2 = self.get_success(
  210. self.handler.get_room_hierarchy(
  211. create_requester(self.user), self.space, from_token=result["next_batch"]
  212. )
  213. )
  214. # Combine the results.
  215. result["rooms"] += result2["rooms"]
  216. self._assert_hierarchy(result, expected)
  217. def test_visibility(self):
  218. """A user not in a space cannot inspect it."""
  219. user2 = self.register_user("user2", "pass")
  220. token2 = self.login("user2", "pass")
  221. # The user can see the space since it is publicly joinable.
  222. expected = [(self.space, [self.room]), (self.room, ())]
  223. result = self.get_success(
  224. self.handler.get_room_hierarchy(create_requester(user2), self.space)
  225. )
  226. self._assert_hierarchy(result, expected)
  227. # If the space is made invite-only, it should no longer be viewable.
  228. self.helper.send_state(
  229. self.space,
  230. event_type=EventTypes.JoinRules,
  231. body={"join_rule": JoinRules.INVITE},
  232. tok=self.token,
  233. )
  234. self.get_failure(
  235. self.handler.get_room_hierarchy(create_requester(user2), self.space),
  236. AuthError,
  237. )
  238. # If the space is made world-readable it should return a result.
  239. self.helper.send_state(
  240. self.space,
  241. event_type=EventTypes.RoomHistoryVisibility,
  242. body={"history_visibility": HistoryVisibility.WORLD_READABLE},
  243. tok=self.token,
  244. )
  245. result = self.get_success(
  246. self.handler.get_room_hierarchy(create_requester(user2), self.space)
  247. )
  248. self._assert_hierarchy(result, expected)
  249. # Make it not world-readable again and confirm it results in an error.
  250. self.helper.send_state(
  251. self.space,
  252. event_type=EventTypes.RoomHistoryVisibility,
  253. body={"history_visibility": HistoryVisibility.JOINED},
  254. tok=self.token,
  255. )
  256. self.get_failure(
  257. self.handler.get_room_hierarchy(create_requester(user2), self.space),
  258. AuthError,
  259. )
  260. # Join the space and results should be returned.
  261. self.helper.invite(self.space, targ=user2, tok=self.token)
  262. self.helper.join(self.space, user2, tok=token2)
  263. result = self.get_success(
  264. self.handler.get_room_hierarchy(create_requester(user2), self.space)
  265. )
  266. self._assert_hierarchy(result, expected)
  267. # Attempting to view an unknown room returns the same error.
  268. self.get_failure(
  269. self.handler.get_room_hierarchy(
  270. create_requester(user2), "#not-a-space:" + self.hs.hostname
  271. ),
  272. AuthError,
  273. )
  274. def test_room_hierarchy_cache(self) -> None:
  275. """In-flight room hierarchy requests are deduplicated."""
  276. # Run two `get_room_hierarchy` calls up until they block.
  277. deferred1 = ensureDeferred(
  278. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  279. )
  280. deferred2 = ensureDeferred(
  281. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  282. )
  283. # Complete the two calls.
  284. result1 = self.get_success(deferred1)
  285. result2 = self.get_success(deferred2)
  286. # Both `get_room_hierarchy` calls should return the same result.
  287. expected = [(self.space, [self.room]), (self.room, ())]
  288. self._assert_hierarchy(result1, expected)
  289. self._assert_hierarchy(result2, expected)
  290. self.assertIs(result1, result2)
  291. # A subsequent `get_room_hierarchy` call should not reuse the result.
  292. result3 = self.get_success(
  293. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  294. )
  295. self._assert_hierarchy(result3, expected)
  296. self.assertIsNot(result1, result3)
  297. def test_room_hierarchy_cache_sharing(self) -> None:
  298. """Room hierarchy responses for different users are not shared."""
  299. user2 = self.register_user("user2", "pass")
  300. # Make the room within the space invite-only.
  301. self.helper.send_state(
  302. self.room,
  303. event_type=EventTypes.JoinRules,
  304. body={"join_rule": JoinRules.INVITE},
  305. tok=self.token,
  306. )
  307. # Run two `get_room_hierarchy` calls for different users up until they block.
  308. deferred1 = ensureDeferred(
  309. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  310. )
  311. deferred2 = ensureDeferred(
  312. self.handler.get_room_hierarchy(create_requester(user2), self.space)
  313. )
  314. # Complete the two calls.
  315. result1 = self.get_success(deferred1)
  316. result2 = self.get_success(deferred2)
  317. # The `get_room_hierarchy` calls should return different results.
  318. self._assert_hierarchy(result1, [(self.space, [self.room]), (self.room, ())])
  319. self._assert_hierarchy(result2, [(self.space, [self.room])])
  320. def _create_room_with_join_rule(
  321. self, join_rule: str, room_version: Optional[str] = None, **extra_content
  322. ) -> str:
  323. """Create a room with the given join rule and add it to the space."""
  324. room_id = self.helper.create_room_as(
  325. self.user,
  326. room_version=room_version,
  327. tok=self.token,
  328. extra_content={
  329. "initial_state": [
  330. {
  331. "type": EventTypes.JoinRules,
  332. "state_key": "",
  333. "content": {
  334. "join_rule": join_rule,
  335. **extra_content,
  336. },
  337. }
  338. ]
  339. },
  340. )
  341. self._add_child(self.space, room_id, self.token)
  342. return room_id
  343. def test_filtering(self):
  344. """
  345. Rooms should be properly filtered to only include rooms the user has access to.
  346. """
  347. user2 = self.register_user("user2", "pass")
  348. token2 = self.login("user2", "pass")
  349. # Create a few rooms which will have different properties.
  350. public_room = self._create_room_with_join_rule(JoinRules.PUBLIC)
  351. knock_room = self._create_room_with_join_rule(
  352. JoinRules.KNOCK, room_version=RoomVersions.V7.identifier
  353. )
  354. not_invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
  355. invited_room = self._create_room_with_join_rule(JoinRules.INVITE)
  356. self.helper.invite(invited_room, targ=user2, tok=self.token)
  357. restricted_room = self._create_room_with_join_rule(
  358. JoinRules.RESTRICTED,
  359. room_version=RoomVersions.V8.identifier,
  360. allow=[],
  361. )
  362. restricted_accessible_room = self._create_room_with_join_rule(
  363. JoinRules.RESTRICTED,
  364. room_version=RoomVersions.V8.identifier,
  365. allow=[
  366. {
  367. "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP,
  368. "room_id": self.space,
  369. "via": [self.hs.hostname],
  370. }
  371. ],
  372. )
  373. world_readable_room = self._create_room_with_join_rule(JoinRules.INVITE)
  374. self.helper.send_state(
  375. world_readable_room,
  376. event_type=EventTypes.RoomHistoryVisibility,
  377. body={"history_visibility": HistoryVisibility.WORLD_READABLE},
  378. tok=self.token,
  379. )
  380. joined_room = self._create_room_with_join_rule(JoinRules.INVITE)
  381. self.helper.invite(joined_room, targ=user2, tok=self.token)
  382. self.helper.join(joined_room, user2, tok=token2)
  383. # Join the space.
  384. self.helper.join(self.space, user2, tok=token2)
  385. expected = [
  386. (
  387. self.space,
  388. [
  389. self.room,
  390. public_room,
  391. knock_room,
  392. not_invited_room,
  393. invited_room,
  394. restricted_room,
  395. restricted_accessible_room,
  396. world_readable_room,
  397. joined_room,
  398. ],
  399. ),
  400. (self.room, ()),
  401. (public_room, ()),
  402. (knock_room, ()),
  403. (invited_room, ()),
  404. (restricted_accessible_room, ()),
  405. (world_readable_room, ()),
  406. (joined_room, ()),
  407. ]
  408. result = self.get_success(
  409. self.handler.get_room_hierarchy(create_requester(user2), self.space)
  410. )
  411. self._assert_hierarchy(result, expected)
  412. def test_complex_space(self):
  413. """
  414. Create a "complex" space to see how it handles things like loops and subspaces.
  415. """
  416. # Create an inaccessible room.
  417. user2 = self.register_user("user2", "pass")
  418. token2 = self.login("user2", "pass")
  419. room2 = self.helper.create_room_as(user2, is_public=False, tok=token2)
  420. # This is a bit odd as "user" is adding a room they don't know about, but
  421. # it works for the tests.
  422. self._add_child(self.space, room2, self.token)
  423. # Create a subspace under the space with an additional room in it.
  424. subspace = self.helper.create_room_as(
  425. self.user,
  426. tok=self.token,
  427. extra_content={
  428. "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
  429. },
  430. )
  431. subroom = self.helper.create_room_as(self.user, tok=self.token)
  432. self._add_child(self.space, subspace, token=self.token)
  433. self._add_child(subspace, subroom, token=self.token)
  434. # Also add the two rooms from the space into this subspace (causing loops).
  435. self._add_child(subspace, self.room, token=self.token)
  436. self._add_child(subspace, room2, self.token)
  437. # The result should include each room a single time and each link.
  438. expected = [
  439. (self.space, [self.room, room2, subspace]),
  440. (self.room, ()),
  441. (subspace, [subroom, self.room, room2]),
  442. (subroom, ()),
  443. ]
  444. result = self.get_success(
  445. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  446. )
  447. self._assert_hierarchy(result, expected)
  448. def test_pagination(self):
  449. """Test simple pagination works."""
  450. room_ids = []
  451. for i in range(1, 10):
  452. room = self.helper.create_room_as(self.user, tok=self.token)
  453. self._add_child(self.space, room, self.token, order=str(i))
  454. room_ids.append(room)
  455. # The room created initially doesn't have an order, so comes last.
  456. room_ids.append(self.room)
  457. result = self.get_success(
  458. self.handler.get_room_hierarchy(
  459. create_requester(self.user), self.space, limit=7
  460. )
  461. )
  462. # The result should have the space and all of the links, plus some of the
  463. # rooms and a pagination token.
  464. expected: List[Tuple[str, Iterable[str]]] = [(self.space, room_ids)]
  465. expected += [(room_id, ()) for room_id in room_ids[:6]]
  466. self._assert_hierarchy(result, expected)
  467. self.assertIn("next_batch", result)
  468. # Check the next page.
  469. result = self.get_success(
  470. self.handler.get_room_hierarchy(
  471. create_requester(self.user),
  472. self.space,
  473. limit=5,
  474. from_token=result["next_batch"],
  475. )
  476. )
  477. # The result should have the space and the room in it, along with a link
  478. # from space -> room.
  479. expected = [(room_id, ()) for room_id in room_ids[6:]]
  480. self._assert_hierarchy(result, expected)
  481. self.assertNotIn("next_batch", result)
  482. def test_invalid_pagination_token(self):
  483. """An invalid pagination token, or changing other parameters, shoudl be rejected."""
  484. room_ids = []
  485. for i in range(1, 10):
  486. room = self.helper.create_room_as(self.user, tok=self.token)
  487. self._add_child(self.space, room, self.token, order=str(i))
  488. room_ids.append(room)
  489. # The room created initially doesn't have an order, so comes last.
  490. room_ids.append(self.room)
  491. result = self.get_success(
  492. self.handler.get_room_hierarchy(
  493. create_requester(self.user), self.space, limit=7
  494. )
  495. )
  496. self.assertIn("next_batch", result)
  497. # Changing the room ID, suggested-only, or max-depth causes an error.
  498. self.get_failure(
  499. self.handler.get_room_hierarchy(
  500. create_requester(self.user), self.room, from_token=result["next_batch"]
  501. ),
  502. SynapseError,
  503. )
  504. self.get_failure(
  505. self.handler.get_room_hierarchy(
  506. create_requester(self.user),
  507. self.space,
  508. suggested_only=True,
  509. from_token=result["next_batch"],
  510. ),
  511. SynapseError,
  512. )
  513. self.get_failure(
  514. self.handler.get_room_hierarchy(
  515. create_requester(self.user),
  516. self.space,
  517. max_depth=0,
  518. from_token=result["next_batch"],
  519. ),
  520. SynapseError,
  521. )
  522. # An invalid token is ignored.
  523. self.get_failure(
  524. self.handler.get_room_hierarchy(
  525. create_requester(self.user), self.space, from_token="foo"
  526. ),
  527. SynapseError,
  528. )
  529. def test_max_depth(self):
  530. """Create a deep tree to test the max depth against."""
  531. spaces = [self.space]
  532. rooms = [self.room]
  533. for _ in range(5):
  534. spaces.append(
  535. self.helper.create_room_as(
  536. self.user,
  537. tok=self.token,
  538. extra_content={
  539. "creation_content": {
  540. EventContentFields.ROOM_TYPE: RoomTypes.SPACE
  541. }
  542. },
  543. )
  544. )
  545. self._add_child(spaces[-2], spaces[-1], self.token)
  546. rooms.append(self.helper.create_room_as(self.user, tok=self.token))
  547. self._add_child(spaces[-1], rooms[-1], self.token)
  548. # Test just the space itself.
  549. result = self.get_success(
  550. self.handler.get_room_hierarchy(
  551. create_requester(self.user), self.space, max_depth=0
  552. )
  553. )
  554. expected: List[Tuple[str, Iterable[str]]] = [(spaces[0], [rooms[0], spaces[1]])]
  555. self._assert_hierarchy(result, expected)
  556. # A single additional layer.
  557. result = self.get_success(
  558. self.handler.get_room_hierarchy(
  559. create_requester(self.user), self.space, max_depth=1
  560. )
  561. )
  562. expected += [
  563. (rooms[0], ()),
  564. (spaces[1], [rooms[1], spaces[2]]),
  565. ]
  566. self._assert_hierarchy(result, expected)
  567. # A few layers.
  568. result = self.get_success(
  569. self.handler.get_room_hierarchy(
  570. create_requester(self.user), self.space, max_depth=3
  571. )
  572. )
  573. expected += [
  574. (rooms[1], ()),
  575. (spaces[2], [rooms[2], spaces[3]]),
  576. (rooms[2], ()),
  577. (spaces[3], [rooms[3], spaces[4]]),
  578. ]
  579. self._assert_hierarchy(result, expected)
  580. def test_unknown_room_version(self):
  581. """
  582. If an room with an unknown room version is encountered it should not cause
  583. the entire summary to skip.
  584. """
  585. # Poke the database and update the room version to an unknown one.
  586. self.get_success(
  587. self.hs.get_datastores().main.db_pool.simple_update(
  588. "rooms",
  589. keyvalues={"room_id": self.room},
  590. updatevalues={"room_version": "unknown-room-version"},
  591. desc="updated-room-version",
  592. )
  593. )
  594. # The result should have only the space, along with a link from space -> room.
  595. expected = [(self.space, [self.room])]
  596. result = self.get_success(
  597. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  598. )
  599. self._assert_hierarchy(result, expected)
  600. def test_fed_complex(self):
  601. """
  602. Return data over federation and ensure that it is handled properly.
  603. """
  604. fed_hostname = self.hs.hostname + "2"
  605. subspace = "#subspace:" + fed_hostname
  606. subroom = "#subroom:" + fed_hostname
  607. # Generate some good data, and some bad data:
  608. #
  609. # * Event *back* to the root room.
  610. # * Unrelated events / rooms
  611. # * Multiple levels of events (in a not-useful order, e.g. grandchild
  612. # events before child events).
  613. # Note that these entries are brief, but should contain enough info.
  614. requested_room_entry = _RoomEntry(
  615. subspace,
  616. {
  617. "room_id": subspace,
  618. "world_readable": True,
  619. "room_type": RoomTypes.SPACE,
  620. },
  621. [
  622. {
  623. "type": EventTypes.SpaceChild,
  624. "room_id": subspace,
  625. "state_key": subroom,
  626. "content": {"via": [fed_hostname]},
  627. }
  628. ],
  629. )
  630. child_room = {
  631. "room_id": subroom,
  632. "world_readable": True,
  633. }
  634. async def summarize_remote_room_hierarchy(_self, room, suggested_only):
  635. return requested_room_entry, {subroom: child_room}, set()
  636. # Add a room to the space which is on another server.
  637. self._add_child(self.space, subspace, self.token)
  638. expected = [
  639. (self.space, [self.room, subspace]),
  640. (self.room, ()),
  641. (subspace, [subroom]),
  642. (subroom, ()),
  643. ]
  644. with mock.patch(
  645. "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
  646. new=summarize_remote_room_hierarchy,
  647. ):
  648. result = self.get_success(
  649. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  650. )
  651. self._assert_hierarchy(result, expected)
  652. def test_fed_filtering(self):
  653. """
  654. Rooms returned over federation should be properly filtered to only include
  655. rooms the user has access to.
  656. """
  657. fed_hostname = self.hs.hostname + "2"
  658. subspace = "#subspace:" + fed_hostname
  659. # Create a few rooms which will have different properties.
  660. public_room = "#public:" + fed_hostname
  661. knock_room = "#knock:" + fed_hostname
  662. not_invited_room = "#not_invited:" + fed_hostname
  663. invited_room = "#invited:" + fed_hostname
  664. restricted_room = "#restricted:" + fed_hostname
  665. restricted_accessible_room = "#restricted_accessible:" + fed_hostname
  666. world_readable_room = "#world_readable:" + fed_hostname
  667. joined_room = self.helper.create_room_as(self.user, tok=self.token)
  668. # Poke an invite over federation into the database.
  669. self._poke_fed_invite(invited_room, "@remote:" + fed_hostname)
  670. # Note that these entries are brief, but should contain enough info.
  671. children_rooms = (
  672. (
  673. public_room,
  674. {
  675. "room_id": public_room,
  676. "world_readable": False,
  677. "join_rules": JoinRules.PUBLIC,
  678. },
  679. ),
  680. (
  681. knock_room,
  682. {
  683. "room_id": knock_room,
  684. "world_readable": False,
  685. "join_rules": JoinRules.KNOCK,
  686. },
  687. ),
  688. (
  689. not_invited_room,
  690. {
  691. "room_id": not_invited_room,
  692. "world_readable": False,
  693. "join_rules": JoinRules.INVITE,
  694. },
  695. ),
  696. (
  697. invited_room,
  698. {
  699. "room_id": invited_room,
  700. "world_readable": False,
  701. "join_rules": JoinRules.INVITE,
  702. },
  703. ),
  704. (
  705. restricted_room,
  706. {
  707. "room_id": restricted_room,
  708. "world_readable": False,
  709. "join_rules": JoinRules.RESTRICTED,
  710. "allowed_room_ids": [],
  711. },
  712. ),
  713. (
  714. restricted_accessible_room,
  715. {
  716. "room_id": restricted_accessible_room,
  717. "world_readable": False,
  718. "join_rules": JoinRules.RESTRICTED,
  719. "allowed_room_ids": [self.room],
  720. },
  721. ),
  722. (
  723. world_readable_room,
  724. {
  725. "room_id": world_readable_room,
  726. "world_readable": True,
  727. "join_rules": JoinRules.INVITE,
  728. },
  729. ),
  730. (
  731. joined_room,
  732. {
  733. "room_id": joined_room,
  734. "world_readable": False,
  735. "join_rules": JoinRules.INVITE,
  736. },
  737. ),
  738. )
  739. subspace_room_entry = _RoomEntry(
  740. subspace,
  741. {
  742. "room_id": subspace,
  743. "world_readable": True,
  744. },
  745. # Place each room in the sub-space.
  746. [
  747. {
  748. "type": EventTypes.SpaceChild,
  749. "room_id": subspace,
  750. "state_key": room_id,
  751. "content": {"via": [fed_hostname]},
  752. }
  753. for room_id, _ in children_rooms
  754. ],
  755. )
  756. async def summarize_remote_room_hierarchy(_self, room, suggested_only):
  757. return subspace_room_entry, dict(children_rooms), set()
  758. # Add a room to the space which is on another server.
  759. self._add_child(self.space, subspace, self.token)
  760. expected = [
  761. (self.space, [self.room, subspace]),
  762. (self.room, ()),
  763. (
  764. subspace,
  765. [
  766. public_room,
  767. knock_room,
  768. not_invited_room,
  769. invited_room,
  770. restricted_room,
  771. restricted_accessible_room,
  772. world_readable_room,
  773. joined_room,
  774. ],
  775. ),
  776. (public_room, ()),
  777. (knock_room, ()),
  778. (invited_room, ()),
  779. (restricted_accessible_room, ()),
  780. (world_readable_room, ()),
  781. (joined_room, ()),
  782. ]
  783. with mock.patch(
  784. "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
  785. new=summarize_remote_room_hierarchy,
  786. ):
  787. result = self.get_success(
  788. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  789. )
  790. self._assert_hierarchy(result, expected)
  791. def test_fed_invited(self):
  792. """
  793. A room which the user was invited to should be included in the response.
  794. This differs from test_fed_filtering in that the room itself is being
  795. queried over federation, instead of it being included as a sub-room of
  796. a space in the response.
  797. """
  798. fed_hostname = self.hs.hostname + "2"
  799. fed_room = "#subroom:" + fed_hostname
  800. # Poke an invite over federation into the database.
  801. self._poke_fed_invite(fed_room, "@remote:" + fed_hostname)
  802. fed_room_entry = _RoomEntry(
  803. fed_room,
  804. {
  805. "room_id": fed_room,
  806. "world_readable": False,
  807. "join_rules": JoinRules.INVITE,
  808. },
  809. )
  810. async def summarize_remote_room_hierarchy(_self, room, suggested_only):
  811. return fed_room_entry, {}, set()
  812. # Add a room to the space which is on another server.
  813. self._add_child(self.space, fed_room, self.token)
  814. expected = [
  815. (self.space, [self.room, fed_room]),
  816. (self.room, ()),
  817. (fed_room, ()),
  818. ]
  819. with mock.patch(
  820. "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
  821. new=summarize_remote_room_hierarchy,
  822. ):
  823. result = self.get_success(
  824. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  825. )
  826. self._assert_hierarchy(result, expected)
  827. def test_fed_caching(self):
  828. """
  829. Federation `/hierarchy` responses should be cached.
  830. """
  831. fed_hostname = self.hs.hostname + "2"
  832. fed_subspace = "#space:" + fed_hostname
  833. fed_room = "#room:" + fed_hostname
  834. # Add a room to the space which is on another server.
  835. self._add_child(self.space, fed_subspace, self.token, via=[fed_hostname])
  836. federation_requests = 0
  837. async def get_room_hierarchy(
  838. _self: TransportLayerClient,
  839. destination: str,
  840. room_id: str,
  841. suggested_only: bool,
  842. ) -> JsonDict:
  843. nonlocal federation_requests
  844. federation_requests += 1
  845. return {
  846. "room": {
  847. "room_id": fed_subspace,
  848. "world_readable": True,
  849. "room_type": RoomTypes.SPACE,
  850. "children_state": [
  851. {
  852. "type": EventTypes.SpaceChild,
  853. "room_id": fed_subspace,
  854. "state_key": fed_room,
  855. "content": {"via": [fed_hostname]},
  856. },
  857. ],
  858. },
  859. "children": [
  860. {
  861. "room_id": fed_room,
  862. "world_readable": True,
  863. },
  864. ],
  865. "inaccessible_children": [],
  866. }
  867. expected = [
  868. (self.space, [self.room, fed_subspace]),
  869. (self.room, ()),
  870. (fed_subspace, [fed_room]),
  871. (fed_room, ()),
  872. ]
  873. with mock.patch(
  874. "synapse.federation.transport.client.TransportLayerClient.get_room_hierarchy",
  875. new=get_room_hierarchy,
  876. ):
  877. result = self.get_success(
  878. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  879. )
  880. self.assertEqual(federation_requests, 1)
  881. self._assert_hierarchy(result, expected)
  882. # The previous federation response should be reused.
  883. result = self.get_success(
  884. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  885. )
  886. self.assertEqual(federation_requests, 1)
  887. self._assert_hierarchy(result, expected)
  888. # Expire the response cache
  889. self.reactor.advance(5 * 60 + 1)
  890. # A new federation request should be made.
  891. result = self.get_success(
  892. self.handler.get_room_hierarchy(create_requester(self.user), self.space)
  893. )
  894. self.assertEqual(federation_requests, 2)
  895. self._assert_hierarchy(result, expected)
  896. class RoomSummaryTestCase(unittest.HomeserverTestCase):
  897. servlets = [
  898. admin.register_servlets_for_client_rest_resource,
  899. room.register_servlets,
  900. login.register_servlets,
  901. ]
  902. def prepare(self, reactor, clock, hs: HomeServer):
  903. self.hs = hs
  904. self.handler = self.hs.get_room_summary_handler()
  905. # Create a user.
  906. self.user = self.register_user("user", "pass")
  907. self.token = self.login("user", "pass")
  908. # Create a simple room.
  909. self.room = self.helper.create_room_as(self.user, tok=self.token)
  910. self.helper.send_state(
  911. self.room,
  912. event_type=EventTypes.JoinRules,
  913. body={"join_rule": JoinRules.INVITE},
  914. tok=self.token,
  915. )
  916. def test_own_room(self):
  917. """Test a simple room created by the requester."""
  918. result = self.get_success(self.handler.get_room_summary(self.user, self.room))
  919. self.assertEqual(result.get("room_id"), self.room)
  920. def test_visibility(self):
  921. """A user not in a private room cannot get its summary."""
  922. user2 = self.register_user("user2", "pass")
  923. token2 = self.login("user2", "pass")
  924. # The user cannot see the room.
  925. self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError)
  926. # If the room is made world-readable it should return a result.
  927. self.helper.send_state(
  928. self.room,
  929. event_type=EventTypes.RoomHistoryVisibility,
  930. body={"history_visibility": HistoryVisibility.WORLD_READABLE},
  931. tok=self.token,
  932. )
  933. result = self.get_success(self.handler.get_room_summary(user2, self.room))
  934. self.assertEqual(result.get("room_id"), self.room)
  935. # Make it not world-readable again and confirm it results in an error.
  936. self.helper.send_state(
  937. self.room,
  938. event_type=EventTypes.RoomHistoryVisibility,
  939. body={"history_visibility": HistoryVisibility.JOINED},
  940. tok=self.token,
  941. )
  942. self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError)
  943. # If the room is made public it should return a result.
  944. self.helper.send_state(
  945. self.room,
  946. event_type=EventTypes.JoinRules,
  947. body={"join_rule": JoinRules.PUBLIC},
  948. tok=self.token,
  949. )
  950. result = self.get_success(self.handler.get_room_summary(user2, self.room))
  951. self.assertEqual(result.get("room_id"), self.room)
  952. # Join the space, make it invite-only again and results should be returned.
  953. self.helper.join(self.room, user2, tok=token2)
  954. self.helper.send_state(
  955. self.room,
  956. event_type=EventTypes.JoinRules,
  957. body={"join_rule": JoinRules.INVITE},
  958. tok=self.token,
  959. )
  960. result = self.get_success(self.handler.get_room_summary(user2, self.room))
  961. self.assertEqual(result.get("room_id"), self.room)