test_presence.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 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 mock import Mock, call
  16. from signedjson.key import generate_signing_key
  17. from synapse.api.constants import EventTypes, Membership, PresenceState
  18. from synapse.events import room_version_to_event_format
  19. from synapse.events.builder import EventBuilder
  20. from synapse.handlers.presence import (
  21. EXTERNAL_PROCESS_EXPIRY,
  22. FEDERATION_PING_INTERVAL,
  23. FEDERATION_TIMEOUT,
  24. IDLE_TIMER,
  25. LAST_ACTIVE_GRANULARITY,
  26. SYNC_ONLINE_TIMEOUT,
  27. handle_timeout,
  28. handle_update,
  29. )
  30. from synapse.rest.client.v1 import room
  31. from synapse.storage.presence import UserPresenceState
  32. from synapse.types import UserID, get_domain_from_id
  33. from tests import unittest
  34. class PresenceUpdateTestCase(unittest.TestCase):
  35. def test_offline_to_online(self):
  36. wheel_timer = Mock()
  37. user_id = "@foo:bar"
  38. now = 5000000
  39. prev_state = UserPresenceState.default(user_id)
  40. new_state = prev_state.copy_and_replace(
  41. state=PresenceState.ONLINE, last_active_ts=now
  42. )
  43. state, persist_and_notify, federation_ping = handle_update(
  44. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  45. )
  46. self.assertTrue(persist_and_notify)
  47. self.assertTrue(state.currently_active)
  48. self.assertEquals(new_state.state, state.state)
  49. self.assertEquals(new_state.status_msg, state.status_msg)
  50. self.assertEquals(state.last_federation_update_ts, now)
  51. self.assertEquals(wheel_timer.insert.call_count, 3)
  52. wheel_timer.insert.assert_has_calls(
  53. [
  54. call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
  55. call(
  56. now=now,
  57. obj=user_id,
  58. then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
  59. ),
  60. call(
  61. now=now,
  62. obj=user_id,
  63. then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
  64. ),
  65. ],
  66. any_order=True,
  67. )
  68. def test_online_to_online(self):
  69. wheel_timer = Mock()
  70. user_id = "@foo:bar"
  71. now = 5000000
  72. prev_state = UserPresenceState.default(user_id)
  73. prev_state = prev_state.copy_and_replace(
  74. state=PresenceState.ONLINE, last_active_ts=now, currently_active=True
  75. )
  76. new_state = prev_state.copy_and_replace(
  77. state=PresenceState.ONLINE, last_active_ts=now
  78. )
  79. state, persist_and_notify, federation_ping = handle_update(
  80. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  81. )
  82. self.assertFalse(persist_and_notify)
  83. self.assertTrue(federation_ping)
  84. self.assertTrue(state.currently_active)
  85. self.assertEquals(new_state.state, state.state)
  86. self.assertEquals(new_state.status_msg, state.status_msg)
  87. self.assertEquals(state.last_federation_update_ts, now)
  88. self.assertEquals(wheel_timer.insert.call_count, 3)
  89. wheel_timer.insert.assert_has_calls(
  90. [
  91. call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
  92. call(
  93. now=now,
  94. obj=user_id,
  95. then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
  96. ),
  97. call(
  98. now=now,
  99. obj=user_id,
  100. then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
  101. ),
  102. ],
  103. any_order=True,
  104. )
  105. def test_online_to_online_last_active_noop(self):
  106. wheel_timer = Mock()
  107. user_id = "@foo:bar"
  108. now = 5000000
  109. prev_state = UserPresenceState.default(user_id)
  110. prev_state = prev_state.copy_and_replace(
  111. state=PresenceState.ONLINE,
  112. last_active_ts=now - LAST_ACTIVE_GRANULARITY - 10,
  113. currently_active=True,
  114. )
  115. new_state = prev_state.copy_and_replace(
  116. state=PresenceState.ONLINE, last_active_ts=now
  117. )
  118. state, persist_and_notify, federation_ping = handle_update(
  119. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  120. )
  121. self.assertFalse(persist_and_notify)
  122. self.assertTrue(federation_ping)
  123. self.assertTrue(state.currently_active)
  124. self.assertEquals(new_state.state, state.state)
  125. self.assertEquals(new_state.status_msg, state.status_msg)
  126. self.assertEquals(state.last_federation_update_ts, now)
  127. self.assertEquals(wheel_timer.insert.call_count, 3)
  128. wheel_timer.insert.assert_has_calls(
  129. [
  130. call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
  131. call(
  132. now=now,
  133. obj=user_id,
  134. then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
  135. ),
  136. call(
  137. now=now,
  138. obj=user_id,
  139. then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
  140. ),
  141. ],
  142. any_order=True,
  143. )
  144. def test_online_to_online_last_active(self):
  145. wheel_timer = Mock()
  146. user_id = "@foo:bar"
  147. now = 5000000
  148. prev_state = UserPresenceState.default(user_id)
  149. prev_state = prev_state.copy_and_replace(
  150. state=PresenceState.ONLINE,
  151. last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1,
  152. currently_active=True,
  153. )
  154. new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE)
  155. state, persist_and_notify, federation_ping = handle_update(
  156. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  157. )
  158. self.assertTrue(persist_and_notify)
  159. self.assertFalse(state.currently_active)
  160. self.assertEquals(new_state.state, state.state)
  161. self.assertEquals(new_state.status_msg, state.status_msg)
  162. self.assertEquals(state.last_federation_update_ts, now)
  163. self.assertEquals(wheel_timer.insert.call_count, 2)
  164. wheel_timer.insert.assert_has_calls(
  165. [
  166. call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
  167. call(
  168. now=now,
  169. obj=user_id,
  170. then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
  171. ),
  172. ],
  173. any_order=True,
  174. )
  175. def test_remote_ping_timer(self):
  176. wheel_timer = Mock()
  177. user_id = "@foo:bar"
  178. now = 5000000
  179. prev_state = UserPresenceState.default(user_id)
  180. prev_state = prev_state.copy_and_replace(
  181. state=PresenceState.ONLINE, last_active_ts=now
  182. )
  183. new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE)
  184. state, persist_and_notify, federation_ping = handle_update(
  185. prev_state, new_state, is_mine=False, wheel_timer=wheel_timer, now=now
  186. )
  187. self.assertFalse(persist_and_notify)
  188. self.assertFalse(federation_ping)
  189. self.assertFalse(state.currently_active)
  190. self.assertEquals(new_state.state, state.state)
  191. self.assertEquals(new_state.status_msg, state.status_msg)
  192. self.assertEquals(wheel_timer.insert.call_count, 1)
  193. wheel_timer.insert.assert_has_calls(
  194. [
  195. call(
  196. now=now,
  197. obj=user_id,
  198. then=new_state.last_federation_update_ts + FEDERATION_TIMEOUT,
  199. )
  200. ],
  201. any_order=True,
  202. )
  203. def test_online_to_offline(self):
  204. wheel_timer = Mock()
  205. user_id = "@foo:bar"
  206. now = 5000000
  207. prev_state = UserPresenceState.default(user_id)
  208. prev_state = prev_state.copy_and_replace(
  209. state=PresenceState.ONLINE, last_active_ts=now, currently_active=True
  210. )
  211. new_state = prev_state.copy_and_replace(state=PresenceState.OFFLINE)
  212. state, persist_and_notify, federation_ping = handle_update(
  213. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  214. )
  215. self.assertTrue(persist_and_notify)
  216. self.assertEquals(new_state.state, state.state)
  217. self.assertEquals(state.last_federation_update_ts, now)
  218. self.assertEquals(wheel_timer.insert.call_count, 0)
  219. def test_online_to_idle(self):
  220. wheel_timer = Mock()
  221. user_id = "@foo:bar"
  222. now = 5000000
  223. prev_state = UserPresenceState.default(user_id)
  224. prev_state = prev_state.copy_and_replace(
  225. state=PresenceState.ONLINE, last_active_ts=now, currently_active=True
  226. )
  227. new_state = prev_state.copy_and_replace(state=PresenceState.UNAVAILABLE)
  228. state, persist_and_notify, federation_ping = handle_update(
  229. prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
  230. )
  231. self.assertTrue(persist_and_notify)
  232. self.assertEquals(new_state.state, state.state)
  233. self.assertEquals(state.last_federation_update_ts, now)
  234. self.assertEquals(new_state.state, state.state)
  235. self.assertEquals(new_state.status_msg, state.status_msg)
  236. self.assertEquals(wheel_timer.insert.call_count, 1)
  237. wheel_timer.insert.assert_has_calls(
  238. [
  239. call(
  240. now=now,
  241. obj=user_id,
  242. then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
  243. )
  244. ],
  245. any_order=True,
  246. )
  247. class PresenceTimeoutTestCase(unittest.TestCase):
  248. def test_idle_timer(self):
  249. user_id = "@foo:bar"
  250. now = 5000000
  251. state = UserPresenceState.default(user_id)
  252. state = state.copy_and_replace(
  253. state=PresenceState.ONLINE,
  254. last_active_ts=now - IDLE_TIMER - 1,
  255. last_user_sync_ts=now,
  256. )
  257. new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now)
  258. self.assertIsNotNone(new_state)
  259. self.assertEquals(new_state.state, PresenceState.UNAVAILABLE)
  260. def test_sync_timeout(self):
  261. user_id = "@foo:bar"
  262. now = 5000000
  263. state = UserPresenceState.default(user_id)
  264. state = state.copy_and_replace(
  265. state=PresenceState.ONLINE,
  266. last_active_ts=0,
  267. last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
  268. )
  269. new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now)
  270. self.assertIsNotNone(new_state)
  271. self.assertEquals(new_state.state, PresenceState.OFFLINE)
  272. def test_sync_online(self):
  273. user_id = "@foo:bar"
  274. now = 5000000
  275. state = UserPresenceState.default(user_id)
  276. state = state.copy_and_replace(
  277. state=PresenceState.ONLINE,
  278. last_active_ts=now - SYNC_ONLINE_TIMEOUT - 1,
  279. last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
  280. )
  281. new_state = handle_timeout(
  282. state, is_mine=True, syncing_user_ids=set([user_id]), now=now
  283. )
  284. self.assertIsNotNone(new_state)
  285. self.assertEquals(new_state.state, PresenceState.ONLINE)
  286. def test_federation_ping(self):
  287. user_id = "@foo:bar"
  288. now = 5000000
  289. state = UserPresenceState.default(user_id)
  290. state = state.copy_and_replace(
  291. state=PresenceState.ONLINE,
  292. last_active_ts=now,
  293. last_user_sync_ts=now,
  294. last_federation_update_ts=now - FEDERATION_PING_INTERVAL - 1,
  295. )
  296. new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now)
  297. self.assertIsNotNone(new_state)
  298. self.assertEquals(new_state, new_state)
  299. def test_no_timeout(self):
  300. user_id = "@foo:bar"
  301. now = 5000000
  302. state = UserPresenceState.default(user_id)
  303. state = state.copy_and_replace(
  304. state=PresenceState.ONLINE,
  305. last_active_ts=now,
  306. last_user_sync_ts=now,
  307. last_federation_update_ts=now,
  308. )
  309. new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now)
  310. self.assertIsNone(new_state)
  311. def test_federation_timeout(self):
  312. user_id = "@foo:bar"
  313. now = 5000000
  314. state = UserPresenceState.default(user_id)
  315. state = state.copy_and_replace(
  316. state=PresenceState.ONLINE,
  317. last_active_ts=now,
  318. last_user_sync_ts=now,
  319. last_federation_update_ts=now - FEDERATION_TIMEOUT - 1,
  320. )
  321. new_state = handle_timeout(
  322. state, is_mine=False, syncing_user_ids=set(), now=now
  323. )
  324. self.assertIsNotNone(new_state)
  325. self.assertEquals(new_state.state, PresenceState.OFFLINE)
  326. def test_last_active(self):
  327. user_id = "@foo:bar"
  328. now = 5000000
  329. state = UserPresenceState.default(user_id)
  330. state = state.copy_and_replace(
  331. state=PresenceState.ONLINE,
  332. last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1,
  333. last_user_sync_ts=now,
  334. last_federation_update_ts=now,
  335. )
  336. new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now)
  337. self.assertIsNotNone(new_state)
  338. self.assertEquals(state, new_state)
  339. class PresenceHandlerTestCase(unittest.HomeserverTestCase):
  340. def prepare(self, reactor, clock, hs):
  341. self.presence_handler = hs.get_presence_handler()
  342. self.clock = hs.get_clock()
  343. def test_external_process_timeout(self):
  344. """Test that if an external process doesn't update the records for a while
  345. we time out their syncing users presence.
  346. """
  347. process_id = 1
  348. user_id = "@test:server"
  349. # Notify handler that a user is now syncing.
  350. self.get_success(
  351. self.presence_handler.update_external_syncs_row(
  352. process_id, user_id, True, self.clock.time_msec()
  353. )
  354. )
  355. # Check that if we wait a while without telling the handler the user has
  356. # stopped syncing that their presence state doesn't get timed out.
  357. self.reactor.advance(EXTERNAL_PROCESS_EXPIRY / 2)
  358. state = self.get_success(
  359. self.presence_handler.get_state(UserID.from_string(user_id))
  360. )
  361. self.assertEqual(state.state, PresenceState.ONLINE)
  362. # Check that if the external process timeout fires, then the syncing
  363. # user gets timed out
  364. self.reactor.advance(EXTERNAL_PROCESS_EXPIRY)
  365. state = self.get_success(
  366. self.presence_handler.get_state(UserID.from_string(user_id))
  367. )
  368. self.assertEqual(state.state, PresenceState.OFFLINE)
  369. class PresenceJoinTestCase(unittest.HomeserverTestCase):
  370. """Tests remote servers get told about presence of users in the room when
  371. they join and when new local users join.
  372. """
  373. user_id = "@test:server"
  374. servlets = [room.register_servlets]
  375. def make_homeserver(self, reactor, clock):
  376. hs = self.setup_test_homeserver(
  377. "server", http_client=None, federation_sender=Mock()
  378. )
  379. return hs
  380. def prepare(self, reactor, clock, hs):
  381. self.federation_sender = hs.get_federation_sender()
  382. self.event_builder_factory = hs.get_event_builder_factory()
  383. self.federation_handler = hs.get_handlers().federation_handler
  384. self.presence_handler = hs.get_presence_handler()
  385. # self.event_builder_for_2 = EventBuilderFactory(hs)
  386. # self.event_builder_for_2.hostname = "test2"
  387. self.store = hs.get_datastore()
  388. self.state = hs.get_state_handler()
  389. self.auth = hs.get_auth()
  390. # We don't actually check signatures in tests, so lets just create a
  391. # random key to use.
  392. self.random_signing_key = generate_signing_key("ver")
  393. def test_remote_joins(self):
  394. # We advance time to something that isn't 0, as we use 0 as a special
  395. # value.
  396. self.reactor.advance(1000000000000)
  397. # Create a room with two local users
  398. room_id = self.helper.create_room_as(self.user_id)
  399. self.helper.join(room_id, "@test2:server")
  400. # Mark test2 as online, test will be offline with a last_active of 0
  401. self.presence_handler.set_state(
  402. UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE}
  403. )
  404. self.reactor.pump([0]) # Wait for presence updates to be handled
  405. #
  406. # Test that a new server gets told about existing presence
  407. #
  408. self.federation_sender.reset_mock()
  409. # Add a new remote server to the room
  410. self._add_new_user(room_id, "@alice:server2")
  411. # We shouldn't have sent out any local presence *updates*
  412. self.federation_sender.send_presence.assert_not_called()
  413. # When new server is joined we send it the local users presence states.
  414. # We expect to only see user @test2:server, as @test:server is offline
  415. # and has a zero last_active_ts
  416. expected_state = self.get_success(
  417. self.presence_handler.current_state_for_user("@test2:server")
  418. )
  419. self.assertEqual(expected_state.state, PresenceState.ONLINE)
  420. self.federation_sender.send_presence_to_destinations.assert_called_once_with(
  421. destinations=["server2"], states=[expected_state]
  422. )
  423. #
  424. # Test that only the new server gets sent presence and not existing servers
  425. #
  426. self.federation_sender.reset_mock()
  427. self._add_new_user(room_id, "@bob:server3")
  428. self.federation_sender.send_presence.assert_not_called()
  429. self.federation_sender.send_presence_to_destinations.assert_called_once_with(
  430. destinations=["server3"], states=[expected_state]
  431. )
  432. def test_remote_gets_presence_when_local_user_joins(self):
  433. # We advance time to something that isn't 0, as we use 0 as a special
  434. # value.
  435. self.reactor.advance(1000000000000)
  436. # Create a room with one local users
  437. room_id = self.helper.create_room_as(self.user_id)
  438. # Mark test as online
  439. self.presence_handler.set_state(
  440. UserID.from_string("@test:server"), {"presence": PresenceState.ONLINE}
  441. )
  442. # Mark test2 as online, test will be offline with a last_active of 0.
  443. # Note we don't join them to the room yet
  444. self.presence_handler.set_state(
  445. UserID.from_string("@test2:server"), {"presence": PresenceState.ONLINE}
  446. )
  447. # Add servers to the room
  448. self._add_new_user(room_id, "@alice:server2")
  449. self._add_new_user(room_id, "@bob:server3")
  450. self.reactor.pump([0]) # Wait for presence updates to be handled
  451. #
  452. # Test that when a local join happens remote servers get told about it
  453. #
  454. self.federation_sender.reset_mock()
  455. # Join local user to room
  456. self.helper.join(room_id, "@test2:server")
  457. self.reactor.pump([0]) # Wait for presence updates to be handled
  458. # We shouldn't have sent out any local presence *updates*
  459. self.federation_sender.send_presence.assert_not_called()
  460. # We expect to only send test2 presence to server2 and server3
  461. expected_state = self.get_success(
  462. self.presence_handler.current_state_for_user("@test2:server")
  463. )
  464. self.assertEqual(expected_state.state, PresenceState.ONLINE)
  465. self.federation_sender.send_presence_to_destinations.assert_called_once_with(
  466. destinations=set(("server2", "server3")), states=[expected_state]
  467. )
  468. def _add_new_user(self, room_id, user_id):
  469. """Add new user to the room by creating an event and poking the federation API.
  470. """
  471. hostname = get_domain_from_id(user_id)
  472. room_version = self.get_success(self.store.get_room_version_id(room_id))
  473. builder = EventBuilder(
  474. state=self.state,
  475. auth=self.auth,
  476. store=self.store,
  477. clock=self.clock,
  478. hostname=hostname,
  479. signing_key=self.random_signing_key,
  480. format_version=room_version_to_event_format(room_version),
  481. room_id=room_id,
  482. type=EventTypes.Member,
  483. sender=user_id,
  484. state_key=user_id,
  485. content={"membership": Membership.JOIN},
  486. )
  487. prev_event_ids = self.get_success(
  488. self.store.get_latest_event_ids_in_room(room_id)
  489. )
  490. event = self.get_success(builder.build(prev_event_ids))
  491. self.get_success(self.federation_handler.on_receive_pdu(hostname, event))
  492. # Check that it was successfully persisted.
  493. self.get_success(self.store.get_event(event.event_id))
  494. self.get_success(self.store.get_event(event.event_id))