test_bulk_push_rule_evaluator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. # Copyright 2022 The Matrix.org Foundation C.I.C.
  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. from typing import Any, Optional
  15. from unittest.mock import patch
  16. from parameterized import parameterized
  17. from twisted.test.proto_helpers import MemoryReactor
  18. from synapse.api.constants import EventContentFields, RelationTypes
  19. from synapse.api.room_versions import RoomVersions
  20. from synapse.push.bulk_push_rule_evaluator import BulkPushRuleEvaluator
  21. from synapse.rest import admin
  22. from synapse.rest.client import login, register, room
  23. from synapse.server import HomeServer
  24. from synapse.types import JsonDict, create_requester
  25. from synapse.util import Clock
  26. from tests.test_utils import simple_async_mock
  27. from tests.unittest import HomeserverTestCase, override_config
  28. class TestBulkPushRuleEvaluator(HomeserverTestCase):
  29. servlets = [
  30. admin.register_servlets_for_client_rest_resource,
  31. room.register_servlets,
  32. login.register_servlets,
  33. register.register_servlets,
  34. ]
  35. def prepare(
  36. self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
  37. ) -> None:
  38. # Create a new user and room.
  39. self.alice = self.register_user("alice", "pass")
  40. self.token = self.login(self.alice, "pass")
  41. self.requester = create_requester(self.alice)
  42. self.room_id = self.helper.create_room_as(
  43. # This is deliberately set to V9, because we want to test the logic which
  44. # handles stringy power levels. Stringy power levels were outlawed in V10.
  45. self.alice,
  46. room_version=RoomVersions.V9.identifier,
  47. tok=self.token,
  48. )
  49. self.event_creation_handler = self.hs.get_event_creation_handler()
  50. @parameterized.expand(
  51. [
  52. # The historically-permitted bad values. Alice's notification should be
  53. # allowed if this threshold is at or below her power level (60)
  54. ("100", False),
  55. ("0", True),
  56. (12.34, True),
  57. (60.0, True),
  58. (67.89, False),
  59. # Values that int(...) would not successfully cast should be ignored.
  60. # The room notification level should then default to 50, per the spec, so
  61. # Alice's notification is allowed.
  62. (None, True),
  63. # We haven't seen `"room": []` or `"room": {}` in the wild (yet), but
  64. # let's check them for paranoia's sake.
  65. ([], True),
  66. ({}, True),
  67. ]
  68. )
  69. def test_action_for_event_by_user_handles_noninteger_room_power_levels(
  70. self, bad_room_level: object, should_permit: bool
  71. ) -> None:
  72. """We should convert strings in `room` to integers before passing to Rust.
  73. Test this as follows:
  74. - Create a room as Alice and invite two other users Bob and Charlie.
  75. - Set PLs so that Alice has PL 60 and `notifications.room` is set to a bad value.
  76. - Have Alice create a message notifying @room.
  77. - Evaluate notification actions for that message. This should not raise.
  78. - Look in the DB to see if that message triggered a highlight for Bob.
  79. The test is parameterised with two arguments:
  80. - the bad power level value for "room", before JSON serisalistion
  81. - whether Bob should expect the message to be highlighted
  82. Reproduces #14060.
  83. A lack of validation: the gift that keeps on giving.
  84. """
  85. # Join another user to the room, so that there is someone to see Alice's
  86. # @room notification.
  87. bob = self.register_user("bob", "pass")
  88. bob_token = self.login(bob, "pass")
  89. self.helper.join(self.room_id, bob, tok=bob_token)
  90. # Alter the power levels in that room to include the bad @room notification
  91. # level. We need to suppress
  92. #
  93. # - canonicaljson validation, because canonicaljson forbids floats;
  94. # - the event jsonschema validation, because it will forbid bad values; and
  95. # - the auth rules checks, because they stop us from creating power levels
  96. # with `"room": null`. (We want to test this case, because we have seen it
  97. # in the wild.)
  98. #
  99. # We have seen stringy and null values for "room" in the wild, so presumably
  100. # some of this validation was missing in the past.
  101. with patch("synapse.events.validator.validate_canonicaljson"), patch(
  102. "synapse.events.validator.jsonschema.validate"
  103. ), patch("synapse.handlers.event_auth.check_state_dependent_auth_rules"):
  104. pl_event_id = self.helper.send_state(
  105. self.room_id,
  106. "m.room.power_levels",
  107. {
  108. "users": {self.alice: 60},
  109. "notifications": {"room": bad_room_level},
  110. },
  111. self.token,
  112. state_key="",
  113. )["event_id"]
  114. # Create a new message event, and try to evaluate it under the dodgy
  115. # power level event.
  116. event, unpersisted_context = self.get_success(
  117. self.event_creation_handler.create_event(
  118. self.requester,
  119. {
  120. "type": "m.room.message",
  121. "room_id": self.room_id,
  122. "content": {
  123. "msgtype": "m.text",
  124. "body": "helo @room",
  125. },
  126. "sender": self.alice,
  127. },
  128. prev_event_ids=[pl_event_id],
  129. )
  130. )
  131. context = self.get_success(unpersisted_context.persist(event))
  132. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  133. # should not raise
  134. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  135. # Did Bob see Alice's @room notification?
  136. highlighted_actions = self.get_success(
  137. self.hs.get_datastores().main.db_pool.simple_select_list(
  138. table="event_push_actions_staging",
  139. keyvalues={
  140. "event_id": event.event_id,
  141. "user_id": bob,
  142. "highlight": 1,
  143. },
  144. retcols=("*",),
  145. desc="get_event_push_actions_staging",
  146. )
  147. )
  148. self.assertEqual(len(highlighted_actions), int(should_permit))
  149. @override_config({"push": {"enabled": False}})
  150. def test_action_for_event_by_user_disabled_by_config(self) -> None:
  151. """Ensure that push rules are not calculated when disabled in the config"""
  152. # Create a new message event which should cause a notification.
  153. event, unpersisted_context = self.get_success(
  154. self.event_creation_handler.create_event(
  155. self.requester,
  156. {
  157. "type": "m.room.message",
  158. "room_id": self.room_id,
  159. "content": {
  160. "msgtype": "m.text",
  161. "body": "helo",
  162. },
  163. "sender": self.alice,
  164. },
  165. )
  166. )
  167. context = self.get_success(unpersisted_context.persist(event))
  168. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  169. # Mock the method which calculates push rules -- we do this instead of
  170. # e.g. checking the results in the database because we want to ensure
  171. # that code isn't even running.
  172. bulk_evaluator._action_for_event_by_user = simple_async_mock() # type: ignore[assignment]
  173. # Ensure no actions are generated!
  174. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  175. bulk_evaluator._action_for_event_by_user.assert_not_called()
  176. def _create_and_process(
  177. self, bulk_evaluator: BulkPushRuleEvaluator, content: Optional[JsonDict] = None
  178. ) -> bool:
  179. """Returns true iff the `mentions` trigger an event push action."""
  180. # Create a new message event which should cause a notification.
  181. event, unpersisted_context = self.get_success(
  182. self.event_creation_handler.create_event(
  183. self.requester,
  184. {
  185. "type": "test",
  186. "room_id": self.room_id,
  187. "content": content or {},
  188. "sender": f"@bob:{self.hs.hostname}",
  189. },
  190. )
  191. )
  192. context = self.get_success(unpersisted_context.persist(event))
  193. # Execute the push rule machinery.
  194. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  195. # If any actions are generated for this event, return true.
  196. result = self.get_success(
  197. self.hs.get_datastores().main.db_pool.simple_select_list(
  198. table="event_push_actions_staging",
  199. keyvalues={"event_id": event.event_id},
  200. retcols=("*",),
  201. desc="get_event_push_actions_staging",
  202. )
  203. )
  204. return len(result) > 0
  205. @override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
  206. def test_user_mentions(self) -> None:
  207. """Test the behavior of an event which includes invalid user mentions."""
  208. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  209. # Not including the mentions field should not notify.
  210. self.assertFalse(self._create_and_process(bulk_evaluator))
  211. # An empty mentions field should not notify.
  212. self.assertFalse(
  213. self._create_and_process(
  214. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {}}
  215. )
  216. )
  217. # Non-dict mentions should be ignored.
  218. mentions: Any
  219. for mentions in (None, True, False, 1, "foo", []):
  220. self.assertFalse(
  221. self._create_and_process(
  222. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
  223. )
  224. )
  225. # A non-list should be ignored.
  226. for mentions in (None, True, False, 1, "foo", {}):
  227. self.assertFalse(
  228. self._create_and_process(
  229. bulk_evaluator,
  230. {EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
  231. )
  232. )
  233. # The Matrix ID appearing anywhere in the list should notify.
  234. self.assertTrue(
  235. self._create_and_process(
  236. bulk_evaluator,
  237. {EventContentFields.MSC3952_MENTIONS: {"user_ids": [self.alice]}},
  238. )
  239. )
  240. self.assertTrue(
  241. self._create_and_process(
  242. bulk_evaluator,
  243. {
  244. EventContentFields.MSC3952_MENTIONS: {
  245. "user_ids": ["@another:test", self.alice]
  246. }
  247. },
  248. )
  249. )
  250. # Duplicate user IDs should notify.
  251. self.assertTrue(
  252. self._create_and_process(
  253. bulk_evaluator,
  254. {
  255. EventContentFields.MSC3952_MENTIONS: {
  256. "user_ids": [self.alice, self.alice]
  257. }
  258. },
  259. )
  260. )
  261. # Invalid entries in the list are ignored.
  262. self.assertFalse(
  263. self._create_and_process(
  264. bulk_evaluator,
  265. {
  266. EventContentFields.MSC3952_MENTIONS: {
  267. "user_ids": [None, True, False, {}, []]
  268. }
  269. },
  270. )
  271. )
  272. self.assertTrue(
  273. self._create_and_process(
  274. bulk_evaluator,
  275. {
  276. EventContentFields.MSC3952_MENTIONS: {
  277. "user_ids": [None, True, False, {}, [], self.alice]
  278. }
  279. },
  280. )
  281. )
  282. # The legacy push rule should not mention if the mentions field exists.
  283. self.assertFalse(
  284. self._create_and_process(
  285. bulk_evaluator,
  286. {
  287. "body": self.alice,
  288. "msgtype": "m.text",
  289. EventContentFields.MSC3952_MENTIONS: {},
  290. },
  291. )
  292. )
  293. @override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
  294. def test_room_mentions(self) -> None:
  295. """Test the behavior of an event which includes invalid room mentions."""
  296. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  297. # Room mentions from those without power should not notify.
  298. self.assertFalse(
  299. self._create_and_process(
  300. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  301. )
  302. )
  303. # Room mentions from those with power should notify.
  304. self.helper.send_state(
  305. self.room_id,
  306. "m.room.power_levels",
  307. {"notifications": {"room": 0}},
  308. self.token,
  309. state_key="",
  310. )
  311. self.assertTrue(
  312. self._create_and_process(
  313. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  314. )
  315. )
  316. # Invalid data should not notify.
  317. mentions: Any
  318. for mentions in (None, False, 1, "foo", [], {}):
  319. self.assertFalse(
  320. self._create_and_process(
  321. bulk_evaluator,
  322. {EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
  323. )
  324. )
  325. # The legacy push rule should not mention if the mentions field exists.
  326. self.assertFalse(
  327. self._create_and_process(
  328. bulk_evaluator,
  329. {
  330. "body": "@room",
  331. "msgtype": "m.text",
  332. EventContentFields.MSC3952_MENTIONS: {},
  333. },
  334. )
  335. )
  336. @override_config({"experimental_features": {"msc3958_supress_edit_notifs": True}})
  337. def test_suppress_edits(self) -> None:
  338. """Under the default push rules, event edits should not generate notifications."""
  339. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  340. # Create & persist an event to use as the parent of the relation.
  341. event, unpersisted_context = self.get_success(
  342. self.event_creation_handler.create_event(
  343. self.requester,
  344. {
  345. "type": "m.room.message",
  346. "room_id": self.room_id,
  347. "content": {
  348. "msgtype": "m.text",
  349. "body": "helo",
  350. },
  351. "sender": self.alice,
  352. },
  353. )
  354. )
  355. context = self.get_success(unpersisted_context.persist(event))
  356. self.get_success(
  357. self.event_creation_handler.handle_new_client_event(
  358. self.requester, events_and_context=[(event, context)]
  359. )
  360. )
  361. # Room mentions from those without power should not notify.
  362. self.assertFalse(
  363. self._create_and_process(
  364. bulk_evaluator,
  365. {
  366. "body": self.alice,
  367. "m.relates_to": {
  368. "rel_type": RelationTypes.REPLACE,
  369. "event_id": event.event_id,
  370. },
  371. },
  372. )
  373. )