test_typing.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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_user_event"])
  53. self.on_new_user_event = mock_notifier.on_new_user_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. self.datastore.get_destination_retry_timings.return_value = (
  78. defer.succeed(DestinationsTable.EntryType("", 0, 0))
  79. )
  80. def get_received_txn_response(*args):
  81. return defer.succeed(None)
  82. self.datastore.get_received_txn_response = get_received_txn_response
  83. self.room_id = "a-room"
  84. # Mock the RoomMemberHandler
  85. hs.handlers.room_member_handler = Mock(spec=[])
  86. self.room_member_handler = hs.handlers.room_member_handler
  87. self.room_members = []
  88. def get_rooms_for_user(user):
  89. if user in self.room_members:
  90. return defer.succeed([self.room_id])
  91. else:
  92. return defer.succeed([])
  93. self.room_member_handler.get_rooms_for_user = get_rooms_for_user
  94. def get_room_members(room_id):
  95. if room_id == self.room_id:
  96. return defer.succeed(self.room_members)
  97. else:
  98. return defer.succeed([])
  99. self.room_member_handler.get_room_members = get_room_members
  100. @defer.inlineCallbacks
  101. def fetch_room_distributions_into(room_id, localusers=None,
  102. remotedomains=None, ignore_user=None):
  103. members = yield get_room_members(room_id)
  104. for member in members:
  105. if ignore_user is not None and member == ignore_user:
  106. continue
  107. if hs.is_mine(member):
  108. if localusers is not None:
  109. localusers.add(member)
  110. else:
  111. if remotedomains is not None:
  112. remotedomains.add(member.domain)
  113. self.room_member_handler.fetch_room_distributions_into = (
  114. fetch_room_distributions_into)
  115. def check_joined_room(room_id, user_id):
  116. if user_id not in [u.to_string() for u in self.room_members]:
  117. raise AuthError(401, "User is not in the room")
  118. self.auth.check_joined_room = check_joined_room
  119. # Some local users to test with
  120. self.u_apple = UserID.from_string("@apple:test")
  121. self.u_banana = UserID.from_string("@banana:test")
  122. # Remote user
  123. self.u_onion = UserID.from_string("@onion:farm")
  124. @defer.inlineCallbacks
  125. def test_started_typing_local(self):
  126. self.room_members = [self.u_apple, self.u_banana]
  127. self.assertEquals(self.event_source.get_current_key(), 0)
  128. yield self.handler.started_typing(
  129. target_user=self.u_apple,
  130. auth_user=self.u_apple,
  131. room_id=self.room_id,
  132. timeout=20000,
  133. )
  134. self.on_new_user_event.assert_has_calls([
  135. call(rooms=[self.room_id]),
  136. ])
  137. self.assertEquals(self.event_source.get_current_key(), 1)
  138. self.assertEquals(
  139. self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
  140. [
  141. {"type": "m.typing",
  142. "room_id": self.room_id,
  143. "content": {
  144. "user_ids": [self.u_apple.to_string()],
  145. }},
  146. ]
  147. )
  148. @defer.inlineCallbacks
  149. def test_started_typing_remote_send(self):
  150. self.room_members = [self.u_apple, self.u_onion]
  151. put_json = self.mock_http_client.put_json
  152. put_json.expect_call_and_return(
  153. call("farm",
  154. path="/_matrix/federation/v1/send/1000000/",
  155. data=_expect_edu("farm", "m.typing",
  156. content={
  157. "room_id": self.room_id,
  158. "user_id": self.u_apple.to_string(),
  159. "typing": True,
  160. }
  161. ),
  162. json_data_callback=ANY,
  163. ),
  164. defer.succeed((200, "OK"))
  165. )
  166. yield self.handler.started_typing(
  167. target_user=self.u_apple,
  168. auth_user=self.u_apple,
  169. room_id=self.room_id,
  170. timeout=20000,
  171. )
  172. yield put_json.await_calls()
  173. @defer.inlineCallbacks
  174. def test_started_typing_remote_recv(self):
  175. self.room_members = [self.u_apple, self.u_onion]
  176. self.assertEquals(self.event_source.get_current_key(), 0)
  177. yield self.mock_federation_resource.trigger("PUT",
  178. "/_matrix/federation/v1/send/1000000/",
  179. _make_edu_json("farm", "m.typing",
  180. content={
  181. "room_id": self.room_id,
  182. "user_id": self.u_onion.to_string(),
  183. "typing": True,
  184. }
  185. )
  186. )
  187. self.on_new_user_event.assert_has_calls([
  188. call(rooms=[self.room_id]),
  189. ])
  190. self.assertEquals(self.event_source.get_current_key(), 1)
  191. self.assertEquals(
  192. self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
  193. [
  194. {"type": "m.typing",
  195. "room_id": self.room_id,
  196. "content": {
  197. "user_ids": [self.u_onion.to_string()],
  198. }},
  199. ]
  200. )
  201. @defer.inlineCallbacks
  202. def test_stopped_typing(self):
  203. self.room_members = [self.u_apple, self.u_banana, self.u_onion]
  204. put_json = self.mock_http_client.put_json
  205. put_json.expect_call_and_return(
  206. call("farm",
  207. path="/_matrix/federation/v1/send/1000000/",
  208. data=_expect_edu("farm", "m.typing",
  209. content={
  210. "room_id": self.room_id,
  211. "user_id": self.u_apple.to_string(),
  212. "typing": False,
  213. }
  214. ),
  215. json_data_callback=ANY,
  216. ),
  217. defer.succeed((200, "OK"))
  218. )
  219. # Gut-wrenching
  220. from synapse.handlers.typing import RoomMember
  221. member = RoomMember(self.room_id, self.u_apple)
  222. self.handler._member_typing_until[member] = 1002000
  223. self.handler._member_typing_timer[member] = (
  224. self.clock.call_later(1002, lambda: 0)
  225. )
  226. self.handler._room_typing[self.room_id] = set((self.u_apple,))
  227. self.assertEquals(self.event_source.get_current_key(), 0)
  228. yield self.handler.stopped_typing(
  229. target_user=self.u_apple,
  230. auth_user=self.u_apple,
  231. room_id=self.room_id,
  232. )
  233. self.on_new_user_event.assert_has_calls([
  234. call(rooms=[self.room_id]),
  235. ])
  236. yield put_json.await_calls()
  237. self.assertEquals(self.event_source.get_current_key(), 1)
  238. self.assertEquals(
  239. self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
  240. [
  241. {"type": "m.typing",
  242. "room_id": self.room_id,
  243. "content": {
  244. "user_ids": [],
  245. }},
  246. ]
  247. )
  248. @defer.inlineCallbacks
  249. def test_typing_timeout(self):
  250. self.room_members = [self.u_apple, self.u_banana]
  251. self.assertEquals(self.event_source.get_current_key(), 0)
  252. yield self.handler.started_typing(
  253. target_user=self.u_apple,
  254. auth_user=self.u_apple,
  255. room_id=self.room_id,
  256. timeout=10000,
  257. )
  258. self.on_new_user_event.assert_has_calls([
  259. call(rooms=[self.room_id]),
  260. ])
  261. self.on_new_user_event.reset_mock()
  262. self.assertEquals(self.event_source.get_current_key(), 1)
  263. self.assertEquals(
  264. self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
  265. [
  266. {"type": "m.typing",
  267. "room_id": self.room_id,
  268. "content": {
  269. "user_ids": [self.u_apple.to_string()],
  270. }},
  271. ]
  272. )
  273. self.clock.advance_time(11)
  274. self.on_new_user_event.assert_has_calls([
  275. call(rooms=[self.room_id]),
  276. ])
  277. self.assertEquals(self.event_source.get_current_key(), 2)
  278. self.assertEquals(
  279. self.event_source.get_new_events_for_user(self.u_apple, 1, None)[0],
  280. [
  281. {"type": "m.typing",
  282. "room_id": self.room_id,
  283. "content": {
  284. "user_ids": [],
  285. }},
  286. ]
  287. )
  288. # SYN-230 - see if we can still set after timeout
  289. yield self.handler.started_typing(
  290. target_user=self.u_apple,
  291. auth_user=self.u_apple,
  292. room_id=self.room_id,
  293. timeout=10000,
  294. )
  295. self.on_new_user_event.assert_has_calls([
  296. call(rooms=[self.room_id]),
  297. ])
  298. self.on_new_user_event.reset_mock()
  299. self.assertEquals(self.event_source.get_current_key(), 3)
  300. self.assertEquals(
  301. self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
  302. [
  303. {"type": "m.typing",
  304. "room_id": self.room_id,
  305. "content": {
  306. "user_ids": [self.u_apple.to_string()],
  307. }},
  308. ]
  309. )