test_room_summary.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  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. (result_room["room_id"], cs["state_key"])
  148. for cs in result_room["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_rule": JoinRules.PUBLIC,
  683. },
  684. ),
  685. (
  686. knock_room,
  687. {
  688. "room_id": knock_room,
  689. "world_readable": False,
  690. "join_rule": JoinRules.KNOCK,
  691. },
  692. ),
  693. (
  694. not_invited_room,
  695. {
  696. "room_id": not_invited_room,
  697. "world_readable": False,
  698. "join_rule": JoinRules.INVITE,
  699. },
  700. ),
  701. (
  702. invited_room,
  703. {
  704. "room_id": invited_room,
  705. "world_readable": False,
  706. "join_rule": JoinRules.INVITE,
  707. },
  708. ),
  709. (
  710. restricted_room,
  711. {
  712. "room_id": restricted_room,
  713. "world_readable": False,
  714. "join_rule": 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_rule": 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_rule": JoinRules.INVITE,
  733. },
  734. ),
  735. (
  736. joined_room,
  737. {
  738. "room_id": joined_room,
  739. "world_readable": False,
  740. "join_rule": 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_rule": 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)
  967. def test_fed(self):
  968. """
  969. Return data over federation and ensure that it is handled properly.
  970. """
  971. fed_hostname = self.hs.hostname + "2"
  972. fed_room = "#fed_room:" + fed_hostname
  973. requested_room_entry = _RoomEntry(
  974. fed_room,
  975. {"room_id": fed_room, "world_readable": True},
  976. )
  977. async def summarize_remote_room_hierarchy(_self, room, suggested_only):
  978. return requested_room_entry, {}, set()
  979. with mock.patch(
  980. "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
  981. new=summarize_remote_room_hierarchy,
  982. ):
  983. result = self.get_success(
  984. self.handler.get_room_summary(
  985. self.user, fed_room, remote_room_hosts=[fed_hostname]
  986. )
  987. )
  988. self.assertEqual(result.get("room_id"), fed_room)