You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1133 lines
41 KiB

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