test_typing.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from tests import unittest
  16. from twisted.internet import defer
  17. from mock import Mock, call, ANY
  18. import json
  19. from ..utils import (
  20. MockHttpResource, MockClock, DeferredMockCallable, setup_test_homeserver
  21. )
  22. from synapse.api.errors import AuthError
  23. from synapse.handlers.typing import TypingNotificationHandler
  24. from synapse.storage.transactions import DestinationsTable
  25. from synapse.types import UserID
  26. def _expect_edu(destination, edu_type, content, origin="test"):
  27. return {
  28. "origin": origin,
  29. "origin_server_ts": 1000000,
  30. "pdus": [],
  31. "edus": [
  32. {
  33. "edu_type": edu_type,
  34. "content": content,
  35. }
  36. ],
  37. "pdu_failures": [],
  38. }
  39. def _make_edu_json(origin, edu_type, content):
  40. return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
  41. class JustTypingNotificationHandlers(object):
  42. def __init__(self, hs):
  43. self.typing_notification_handler = TypingNotificationHandler(hs)
  44. class TypingNotificationsTestCase(unittest.TestCase):
  45. """Tests typing notifications to rooms."""
  46. @defer.inlineCallbacks
  47. def setUp(self):
  48. self.clock = MockClock()
  49. self.mock_http_client = Mock(spec=[])
  50. self.mock_http_client.put_json = DeferredMockCallable()
  51. self.mock_federation_resource = MockHttpResource()
  52. mock_notifier = Mock(spec=["on_new_event"])
  53. self.on_new_event = mock_notifier.on_new_event
  54. self.auth = Mock(spec=[])
  55. hs = yield setup_test_homeserver(
  56. auth=self.auth,
  57. clock=self.clock,
  58. datastore=Mock(spec=[
  59. # Bits that Federation needs
  60. "prep_send_transaction",
  61. "delivered_txn",
  62. "get_received_txn_response",
  63. "set_received_txn_response",
  64. "get_destination_retry_timings",
  65. ]),
  66. handlers=None,
  67. notifier=mock_notifier,
  68. resource_for_client=Mock(),
  69. resource_for_federation=self.mock_federation_resource,
  70. http_client=self.mock_http_client,
  71. keyring=Mock(),
  72. )
  73. hs.handlers = JustTypingNotificationHandlers(hs)
  74. self.handler = hs.get_handlers().typing_notification_handler
  75. self.event_source = hs.get_event_sources().sources["typing"]
  76. self.datastore = hs.get_datastore()
  77. retry_timings_res = {
  78. "destination": "",
  79. "retry_last_ts": 0,
  80. "retry_interval": 0,
  81. }
  82. self.datastore.get_destination_retry_timings.return_value = (
  83. defer.succeed(retry_timings_res)
  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_id = "a-room"
  89. # Mock the RoomMemberHandler
  90. hs.handlers.room_member_handler = Mock(spec=[])
  91. self.room_member_handler = hs.handlers.room_member_handler
  92. self.room_members = []
  93. def get_rooms_for_user(user):
  94. if user in self.room_members:
  95. return defer.succeed([self.room_id])
  96. else:
  97. return defer.succeed([])
  98. self.room_member_handler.get_rooms_for_user = get_rooms_for_user
  99. def get_room_members(room_id):
  100. if room_id == self.room_id:
  101. return defer.succeed(self.room_members)
  102. else:
  103. return defer.succeed([])
  104. self.room_member_handler.get_room_members = get_room_members
  105. def get_joined_rooms_for_user(user):
  106. if user in self.room_members:
  107. return defer.succeed([self.room_id])
  108. else:
  109. return defer.succeed([])
  110. self.room_member_handler.get_joined_rooms_for_user = get_joined_rooms_for_user
  111. @defer.inlineCallbacks
  112. def fetch_room_distributions_into(room_id, localusers=None,
  113. remotedomains=None, ignore_user=None):
  114. members = yield get_room_members(room_id)
  115. for member in members:
  116. if ignore_user is not None and member == ignore_user:
  117. continue
  118. if hs.is_mine(member):
  119. if localusers is not None:
  120. localusers.add(member)
  121. else:
  122. if remotedomains is not None:
  123. remotedomains.add(member.domain)
  124. self.room_member_handler.fetch_room_distributions_into = (
  125. fetch_room_distributions_into)
  126. def check_joined_room(room_id, user_id):
  127. if user_id not in [u.to_string() for u in self.room_members]:
  128. raise AuthError(401, "User is not in the room")
  129. self.auth.check_joined_room = check_joined_room
  130. # Some local users to test with
  131. self.u_apple = UserID.from_string("@apple:test")
  132. self.u_banana = UserID.from_string("@banana:test")
  133. # Remote user
  134. self.u_onion = UserID.from_string("@onion:farm")
  135. @defer.inlineCallbacks
  136. def test_started_typing_local(self):
  137. self.room_members = [self.u_apple, self.u_banana]
  138. self.assertEquals(self.event_source.get_current_key(), 0)
  139. yield self.handler.started_typing(
  140. target_user=self.u_apple,
  141. auth_user=self.u_apple,
  142. room_id=self.room_id,
  143. timeout=20000,
  144. )
  145. self.on_new_event.assert_has_calls([
  146. call('typing_key', 1, rooms=[self.room_id]),
  147. ])
  148. self.assertEquals(self.event_source.get_current_key(), 1)
  149. events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None)
  150. self.assertEquals(
  151. events[0],
  152. [
  153. {"type": "m.typing",
  154. "room_id": self.room_id,
  155. "content": {
  156. "user_ids": [self.u_apple.to_string()],
  157. }},
  158. ]
  159. )
  160. @defer.inlineCallbacks
  161. def test_started_typing_remote_send(self):
  162. self.room_members = [self.u_apple, self.u_onion]
  163. put_json = self.mock_http_client.put_json
  164. put_json.expect_call_and_return(
  165. call("farm",
  166. path="/_matrix/federation/v1/send/1000000/",
  167. data=_expect_edu("farm", "m.typing",
  168. content={
  169. "room_id": self.room_id,
  170. "user_id": self.u_apple.to_string(),
  171. "typing": True,
  172. }
  173. ),
  174. json_data_callback=ANY,
  175. ),
  176. defer.succeed((200, "OK"))
  177. )
  178. yield self.handler.started_typing(
  179. target_user=self.u_apple,
  180. auth_user=self.u_apple,
  181. room_id=self.room_id,
  182. timeout=20000,
  183. )
  184. yield put_json.await_calls()
  185. @defer.inlineCallbacks
  186. def test_started_typing_remote_recv(self):
  187. self.room_members = [self.u_apple, self.u_onion]
  188. self.assertEquals(self.event_source.get_current_key(), 0)
  189. yield self.mock_federation_resource.trigger("PUT",
  190. "/_matrix/federation/v1/send/1000000/",
  191. _make_edu_json("farm", "m.typing",
  192. content={
  193. "room_id": self.room_id,
  194. "user_id": self.u_onion.to_string(),
  195. "typing": True,
  196. }
  197. )
  198. )
  199. self.on_new_event.assert_has_calls([
  200. call('typing_key', 1, rooms=[self.room_id]),
  201. ])
  202. self.assertEquals(self.event_source.get_current_key(), 1)
  203. events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None)
  204. self.assertEquals(
  205. events[0],
  206. [
  207. {"type": "m.typing",
  208. "room_id": self.room_id,
  209. "content": {
  210. "user_ids": [self.u_onion.to_string()],
  211. }},
  212. ]
  213. )
  214. @defer.inlineCallbacks
  215. def test_stopped_typing(self):
  216. self.room_members = [self.u_apple, self.u_banana, self.u_onion]
  217. put_json = self.mock_http_client.put_json
  218. put_json.expect_call_and_return(
  219. call("farm",
  220. path="/_matrix/federation/v1/send/1000000/",
  221. data=_expect_edu("farm", "m.typing",
  222. content={
  223. "room_id": self.room_id,
  224. "user_id": self.u_apple.to_string(),
  225. "typing": False,
  226. }
  227. ),
  228. json_data_callback=ANY,
  229. ),
  230. defer.succeed((200, "OK"))
  231. )
  232. # Gut-wrenching
  233. from synapse.handlers.typing import RoomMember
  234. member = RoomMember(self.room_id, self.u_apple)
  235. self.handler._member_typing_until[member] = 1002000
  236. self.handler._member_typing_timer[member] = (
  237. self.clock.call_later(1002, lambda: 0)
  238. )
  239. self.handler._room_typing[self.room_id] = set((self.u_apple,))
  240. self.assertEquals(self.event_source.get_current_key(), 0)
  241. yield self.handler.stopped_typing(
  242. target_user=self.u_apple,
  243. auth_user=self.u_apple,
  244. room_id=self.room_id,
  245. )
  246. self.on_new_event.assert_has_calls([
  247. call('typing_key', 1, rooms=[self.room_id]),
  248. ])
  249. yield put_json.await_calls()
  250. self.assertEquals(self.event_source.get_current_key(), 1)
  251. events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None)
  252. self.assertEquals(
  253. events[0],
  254. [
  255. {"type": "m.typing",
  256. "room_id": self.room_id,
  257. "content": {
  258. "user_ids": [],
  259. }},
  260. ]
  261. )
  262. @defer.inlineCallbacks
  263. def test_typing_timeout(self):
  264. self.room_members = [self.u_apple, self.u_banana]
  265. self.assertEquals(self.event_source.get_current_key(), 0)
  266. yield self.handler.started_typing(
  267. target_user=self.u_apple,
  268. auth_user=self.u_apple,
  269. room_id=self.room_id,
  270. timeout=10000,
  271. )
  272. self.on_new_event.assert_has_calls([
  273. call('typing_key', 1, rooms=[self.room_id]),
  274. ])
  275. self.on_new_event.reset_mock()
  276. self.assertEquals(self.event_source.get_current_key(), 1)
  277. events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None)
  278. self.assertEquals(
  279. events[0],
  280. [
  281. {"type": "m.typing",
  282. "room_id": self.room_id,
  283. "content": {
  284. "user_ids": [self.u_apple.to_string()],
  285. }},
  286. ]
  287. )
  288. self.clock.advance_time(11)
  289. self.on_new_event.assert_has_calls([
  290. call('typing_key', 2, rooms=[self.room_id]),
  291. ])
  292. self.assertEquals(self.event_source.get_current_key(), 2)
  293. events = yield self.event_source.get_new_events_for_user(self.u_apple, 1, None)
  294. self.assertEquals(
  295. events[0],
  296. [
  297. {"type": "m.typing",
  298. "room_id": self.room_id,
  299. "content": {
  300. "user_ids": [],
  301. }},
  302. ]
  303. )
  304. # SYN-230 - see if we can still set after timeout
  305. yield self.handler.started_typing(
  306. target_user=self.u_apple,
  307. auth_user=self.u_apple,
  308. room_id=self.room_id,
  309. timeout=10000,
  310. )
  311. self.on_new_event.assert_has_calls([
  312. call('typing_key', 3, rooms=[self.room_id]),
  313. ])
  314. self.on_new_event.reset_mock()
  315. self.assertEquals(self.event_source.get_current_key(), 3)
  316. events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None)
  317. self.assertEquals(
  318. events[0],
  319. [
  320. {"type": "m.typing",
  321. "room_id": self.room_id,
  322. "content": {
  323. "user_ids": [self.u_apple.to_string()],
  324. }},
  325. ]
  326. )