1
0

test_typing.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. # Copyright 2014-2016 OpenMarket Ltd
  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. import json
  15. from typing import Dict, List, Set
  16. from unittest.mock import ANY, Mock, call
  17. from twisted.test.proto_helpers import MemoryReactor
  18. from twisted.web.resource import Resource
  19. from synapse.api.constants import EduTypes
  20. from synapse.api.errors import AuthError
  21. from synapse.federation.transport.server import TransportLayerServer
  22. from synapse.handlers.typing import TypingWriterHandler
  23. from synapse.server import HomeServer
  24. from synapse.types import JsonDict, Requester, UserID, create_requester
  25. from synapse.util import Clock
  26. from tests import unittest
  27. from tests.server import ThreadedMemoryReactorClock
  28. from tests.test_utils import make_awaitable
  29. from tests.unittest import override_config
  30. # Some local users to test with
  31. U_APPLE = UserID.from_string("@apple:test")
  32. U_BANANA = UserID.from_string("@banana:test")
  33. # Remote user
  34. U_ONION = UserID.from_string("@onion:farm")
  35. # Test room id
  36. ROOM_ID = "a-room"
  37. # Room we're not in
  38. OTHER_ROOM_ID = "another-room"
  39. def _expect_edu_transaction(
  40. edu_type: str, content: JsonDict, origin: str = "test"
  41. ) -> JsonDict:
  42. return {
  43. "origin": origin,
  44. "origin_server_ts": 1000000,
  45. "pdus": [],
  46. "edus": [{"edu_type": edu_type, "content": content}],
  47. }
  48. def _make_edu_transaction_json(edu_type: str, content: JsonDict) -> bytes:
  49. return json.dumps(_expect_edu_transaction(edu_type, content)).encode("utf8")
  50. class TypingNotificationsTestCase(unittest.HomeserverTestCase):
  51. def make_homeserver(
  52. self,
  53. reactor: ThreadedMemoryReactorClock,
  54. clock: Clock,
  55. ) -> HomeServer:
  56. # we mock out the keyring so as to skip the authentication check on the
  57. # federation API call.
  58. mock_keyring = Mock(spec=["verify_json_for_server"])
  59. mock_keyring.verify_json_for_server.return_value = make_awaitable(True)
  60. # we mock out the federation client too
  61. self.mock_federation_client = Mock(spec=["put_json"])
  62. self.mock_federation_client.put_json.return_value = make_awaitable((200, "OK"))
  63. # the tests assume that we are starting at unix time 1000
  64. reactor.pump((1000,))
  65. self.mock_hs_notifier = Mock()
  66. hs = self.setup_test_homeserver(
  67. notifier=self.mock_hs_notifier,
  68. federation_http_client=self.mock_federation_client,
  69. keyring=mock_keyring,
  70. replication_streams={},
  71. )
  72. return hs
  73. def create_resource_dict(self) -> Dict[str, Resource]:
  74. d = super().create_resource_dict()
  75. d["/_matrix/federation"] = TransportLayerServer(self.hs)
  76. return d
  77. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  78. self.on_new_event = self.mock_hs_notifier.on_new_event
  79. # hs.get_typing_handler will return a TypingWriterHandler when calling it
  80. # from the main process, and a FollowerTypingHandler on workers.
  81. # We rely on methods only available on the former, so assert we have the
  82. # correct type here. We have to assign self.handler after the assert,
  83. # otherwise mypy will treat it as a FollowerTypingHandler
  84. handler = hs.get_typing_handler()
  85. assert isinstance(handler, TypingWriterHandler)
  86. self.handler = handler
  87. self.event_source = hs.get_event_sources().sources.typing
  88. self.datastore = hs.get_datastores().main
  89. self.datastore.get_destination_retry_timings = Mock(
  90. return_value=make_awaitable(None)
  91. )
  92. self.datastore.get_device_updates_by_remote = Mock( # type: ignore[assignment]
  93. return_value=make_awaitable((0, []))
  94. )
  95. self.datastore.get_destination_last_successful_stream_ordering = Mock( # type: ignore[assignment]
  96. return_value=make_awaitable(None)
  97. )
  98. self.datastore.get_received_txn_response = Mock( # type: ignore[assignment]
  99. return_value=make_awaitable(None)
  100. )
  101. self.room_members: List[UserID] = []
  102. async def check_user_in_room(room_id: str, requester: Requester) -> None:
  103. if requester.user.to_string() not in [
  104. u.to_string() for u in self.room_members
  105. ]:
  106. raise AuthError(401, "User is not in the room")
  107. return None
  108. hs.get_auth().check_user_in_room = Mock( # type: ignore[assignment]
  109. side_effect=check_user_in_room
  110. )
  111. async def check_host_in_room(room_id: str, server_name: str) -> bool:
  112. return room_id == ROOM_ID
  113. hs.get_event_auth_handler().is_host_in_room = Mock( # type: ignore[assignment]
  114. side_effect=check_host_in_room
  115. )
  116. async def get_current_hosts_in_room(room_id: str) -> Set[str]:
  117. return {member.domain for member in self.room_members}
  118. hs.get_storage_controllers().state.get_current_hosts_in_room = Mock( # type: ignore[assignment]
  119. side_effect=get_current_hosts_in_room
  120. )
  121. hs.get_storage_controllers().state.get_current_hosts_in_room_or_partial_state_approximation = Mock( # type: ignore[assignment]
  122. side_effect=get_current_hosts_in_room
  123. )
  124. async def get_users_in_room(room_id: str) -> Set[str]:
  125. return {str(u) for u in self.room_members}
  126. self.datastore.get_users_in_room = Mock(side_effect=get_users_in_room)
  127. self.datastore.get_user_directory_stream_pos = Mock( # type: ignore[assignment]
  128. side_effect=(
  129. # we deliberately return a non-None stream pos to avoid
  130. # doing an initial_sync
  131. lambda: make_awaitable(1)
  132. )
  133. )
  134. self.datastore.get_partial_current_state_deltas = Mock(return_value=(0, None)) # type: ignore[assignment]
  135. self.datastore.get_to_device_stream_token = Mock( # type: ignore[assignment]
  136. side_effect=lambda: 0
  137. )
  138. self.datastore.get_new_device_msgs_for_remote = Mock( # type: ignore[assignment]
  139. side_effect=lambda *args, **kargs: make_awaitable(([], 0))
  140. )
  141. self.datastore.delete_device_msgs_for_remote = Mock( # type: ignore[assignment]
  142. side_effect=lambda *args, **kargs: make_awaitable(None)
  143. )
  144. self.datastore.set_received_txn_response = Mock( # type: ignore[assignment]
  145. side_effect=lambda *args, **kwargs: make_awaitable(None)
  146. )
  147. def test_started_typing_local(self) -> None:
  148. self.room_members = [U_APPLE, U_BANANA]
  149. self.assertEqual(self.event_source.get_current_key(), 0)
  150. self.get_success(
  151. self.handler.started_typing(
  152. target_user=U_APPLE,
  153. requester=create_requester(U_APPLE),
  154. room_id=ROOM_ID,
  155. timeout=20000,
  156. )
  157. )
  158. self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])])
  159. self.assertEqual(self.event_source.get_current_key(), 1)
  160. events = self.get_success(
  161. self.event_source.get_new_events(
  162. user=U_APPLE, from_key=0, limit=0, room_ids=[ROOM_ID], is_guest=False
  163. )
  164. )
  165. self.assertEqual(
  166. events[0],
  167. [
  168. {
  169. "type": EduTypes.TYPING,
  170. "room_id": ROOM_ID,
  171. "content": {"user_ids": [U_APPLE.to_string()]},
  172. }
  173. ],
  174. )
  175. # Enable federation sending on the main process.
  176. @override_config({"federation_sender_instances": None})
  177. def test_started_typing_remote_send(self) -> None:
  178. self.room_members = [U_APPLE, U_ONION]
  179. self.get_success(
  180. self.handler.started_typing(
  181. target_user=U_APPLE,
  182. requester=create_requester(U_APPLE),
  183. room_id=ROOM_ID,
  184. timeout=20000,
  185. )
  186. )
  187. self.mock_federation_client.put_json.assert_called_once_with(
  188. "farm",
  189. path="/_matrix/federation/v1/send/1000000",
  190. data=_expect_edu_transaction(
  191. EduTypes.TYPING,
  192. content={
  193. "room_id": ROOM_ID,
  194. "user_id": U_APPLE.to_string(),
  195. "typing": True,
  196. },
  197. ),
  198. json_data_callback=ANY,
  199. long_retries=True,
  200. backoff_on_404=True,
  201. try_trailing_slash_on_400=True,
  202. )
  203. def test_started_typing_remote_recv(self) -> None:
  204. self.room_members = [U_APPLE, U_ONION]
  205. self.assertEqual(self.event_source.get_current_key(), 0)
  206. channel = self.make_request(
  207. "PUT",
  208. "/_matrix/federation/v1/send/1000000",
  209. _make_edu_transaction_json(
  210. EduTypes.TYPING,
  211. content={
  212. "room_id": ROOM_ID,
  213. "user_id": U_ONION.to_string(),
  214. "typing": True,
  215. },
  216. ),
  217. federation_auth_origin=b"farm",
  218. )
  219. self.assertEqual(channel.code, 200)
  220. self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])])
  221. self.assertEqual(self.event_source.get_current_key(), 1)
  222. events = self.get_success(
  223. self.event_source.get_new_events(
  224. user=U_APPLE, from_key=0, limit=0, room_ids=[ROOM_ID], is_guest=False
  225. )
  226. )
  227. self.assertEqual(
  228. events[0],
  229. [
  230. {
  231. "type": EduTypes.TYPING,
  232. "room_id": ROOM_ID,
  233. "content": {"user_ids": [U_ONION.to_string()]},
  234. }
  235. ],
  236. )
  237. def test_started_typing_remote_recv_not_in_room(self) -> None:
  238. self.room_members = [U_APPLE, U_ONION]
  239. self.assertEqual(self.event_source.get_current_key(), 0)
  240. channel = self.make_request(
  241. "PUT",
  242. "/_matrix/federation/v1/send/1000000",
  243. _make_edu_transaction_json(
  244. EduTypes.TYPING,
  245. content={
  246. "room_id": OTHER_ROOM_ID,
  247. "user_id": U_ONION.to_string(),
  248. "typing": True,
  249. },
  250. ),
  251. federation_auth_origin=b"farm",
  252. )
  253. self.assertEqual(channel.code, 200)
  254. self.on_new_event.assert_not_called()
  255. self.assertEqual(self.event_source.get_current_key(), 0)
  256. events = self.get_success(
  257. self.event_source.get_new_events(
  258. user=U_APPLE,
  259. from_key=0,
  260. limit=0,
  261. room_ids=[OTHER_ROOM_ID],
  262. is_guest=False,
  263. )
  264. )
  265. self.assertEqual(events[0], [])
  266. self.assertEqual(events[1], 0)
  267. # Enable federation sending on the main process.
  268. @override_config({"federation_sender_instances": None})
  269. def test_stopped_typing(self) -> None:
  270. self.room_members = [U_APPLE, U_BANANA, U_ONION]
  271. # Gut-wrenching
  272. from synapse.handlers.typing import RoomMember
  273. member = RoomMember(ROOM_ID, U_APPLE.to_string())
  274. self.handler._member_typing_until[member] = 1002000
  275. self.handler._room_typing[ROOM_ID] = {U_APPLE.to_string()}
  276. self.assertEqual(self.event_source.get_current_key(), 0)
  277. self.get_success(
  278. self.handler.stopped_typing(
  279. target_user=U_APPLE,
  280. requester=create_requester(U_APPLE),
  281. room_id=ROOM_ID,
  282. )
  283. )
  284. self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])])
  285. self.mock_federation_client.put_json.assert_called_once_with(
  286. "farm",
  287. path="/_matrix/federation/v1/send/1000000",
  288. data=_expect_edu_transaction(
  289. EduTypes.TYPING,
  290. content={
  291. "room_id": ROOM_ID,
  292. "user_id": U_APPLE.to_string(),
  293. "typing": False,
  294. },
  295. ),
  296. json_data_callback=ANY,
  297. long_retries=True,
  298. backoff_on_404=True,
  299. try_trailing_slash_on_400=True,
  300. )
  301. self.assertEqual(self.event_source.get_current_key(), 1)
  302. events = self.get_success(
  303. self.event_source.get_new_events(
  304. user=U_APPLE, from_key=0, limit=0, room_ids=[ROOM_ID], is_guest=False
  305. )
  306. )
  307. self.assertEqual(
  308. events[0],
  309. [
  310. {
  311. "type": EduTypes.TYPING,
  312. "room_id": ROOM_ID,
  313. "content": {"user_ids": []},
  314. }
  315. ],
  316. )
  317. def test_typing_timeout(self) -> None:
  318. self.room_members = [U_APPLE, U_BANANA]
  319. self.assertEqual(self.event_source.get_current_key(), 0)
  320. self.get_success(
  321. self.handler.started_typing(
  322. target_user=U_APPLE,
  323. requester=create_requester(U_APPLE),
  324. room_id=ROOM_ID,
  325. timeout=10000,
  326. )
  327. )
  328. self.on_new_event.assert_has_calls([call("typing_key", 1, rooms=[ROOM_ID])])
  329. self.on_new_event.reset_mock()
  330. self.assertEqual(self.event_source.get_current_key(), 1)
  331. events = self.get_success(
  332. self.event_source.get_new_events(
  333. user=U_APPLE,
  334. from_key=0,
  335. limit=0,
  336. room_ids=[ROOM_ID],
  337. is_guest=False,
  338. )
  339. )
  340. self.assertEqual(
  341. events[0],
  342. [
  343. {
  344. "type": EduTypes.TYPING,
  345. "room_id": ROOM_ID,
  346. "content": {"user_ids": [U_APPLE.to_string()]},
  347. }
  348. ],
  349. )
  350. self.reactor.pump([16])
  351. self.on_new_event.assert_has_calls([call("typing_key", 2, rooms=[ROOM_ID])])
  352. self.assertEqual(self.event_source.get_current_key(), 2)
  353. events = self.get_success(
  354. self.event_source.get_new_events(
  355. user=U_APPLE,
  356. from_key=1,
  357. limit=0,
  358. room_ids=[ROOM_ID],
  359. is_guest=False,
  360. )
  361. )
  362. self.assertEqual(
  363. events[0],
  364. [
  365. {
  366. "type": EduTypes.TYPING,
  367. "room_id": ROOM_ID,
  368. "content": {"user_ids": []},
  369. }
  370. ],
  371. )
  372. # SYN-230 - see if we can still set after timeout
  373. self.get_success(
  374. self.handler.started_typing(
  375. target_user=U_APPLE,
  376. requester=create_requester(U_APPLE),
  377. room_id=ROOM_ID,
  378. timeout=10000,
  379. )
  380. )
  381. self.on_new_event.assert_has_calls([call("typing_key", 3, rooms=[ROOM_ID])])
  382. self.on_new_event.reset_mock()
  383. self.assertEqual(self.event_source.get_current_key(), 3)
  384. events = self.get_success(
  385. self.event_source.get_new_events(
  386. user=U_APPLE,
  387. from_key=0,
  388. limit=0,
  389. room_ids=[ROOM_ID],
  390. is_guest=False,
  391. )
  392. )
  393. self.assertEqual(
  394. events[0],
  395. [
  396. {
  397. "type": EduTypes.TYPING,
  398. "room_id": ROOM_ID,
  399. "content": {"user_ids": [U_APPLE.to_string()]},
  400. }
  401. ],
  402. )