test_typing.py 14 KB

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