test_room_summary.py 39 KB

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