test_typing.py 16 KB

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