test_bulk_push_rule_evaluator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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(
  206. {
  207. "experimental_features": {
  208. "msc3952_intentional_mentions": True,
  209. "msc3966_exact_event_property_contains": True,
  210. }
  211. }
  212. )
  213. def test_user_mentions(self) -> None:
  214. """Test the behavior of an event which includes invalid user mentions."""
  215. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  216. # Not including the mentions field should not notify.
  217. self.assertFalse(self._create_and_process(bulk_evaluator))
  218. # An empty mentions field should not notify.
  219. self.assertFalse(
  220. self._create_and_process(
  221. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {}}
  222. )
  223. )
  224. # Non-dict mentions should be ignored.
  225. mentions: Any
  226. for mentions in (None, True, False, 1, "foo", []):
  227. self.assertFalse(
  228. self._create_and_process(
  229. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
  230. )
  231. )
  232. # A non-list should be ignored.
  233. for mentions in (None, True, False, 1, "foo", {}):
  234. self.assertFalse(
  235. self._create_and_process(
  236. bulk_evaluator,
  237. {EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
  238. )
  239. )
  240. # The Matrix ID appearing anywhere in the list should notify.
  241. self.assertTrue(
  242. self._create_and_process(
  243. bulk_evaluator,
  244. {EventContentFields.MSC3952_MENTIONS: {"user_ids": [self.alice]}},
  245. )
  246. )
  247. self.assertTrue(
  248. self._create_and_process(
  249. bulk_evaluator,
  250. {
  251. EventContentFields.MSC3952_MENTIONS: {
  252. "user_ids": ["@another:test", self.alice]
  253. }
  254. },
  255. )
  256. )
  257. # Duplicate user IDs should notify.
  258. self.assertTrue(
  259. self._create_and_process(
  260. bulk_evaluator,
  261. {
  262. EventContentFields.MSC3952_MENTIONS: {
  263. "user_ids": [self.alice, self.alice]
  264. }
  265. },
  266. )
  267. )
  268. # Invalid entries in the list are ignored.
  269. self.assertFalse(
  270. self._create_and_process(
  271. bulk_evaluator,
  272. {
  273. EventContentFields.MSC3952_MENTIONS: {
  274. "user_ids": [None, True, False, {}, []]
  275. }
  276. },
  277. )
  278. )
  279. self.assertTrue(
  280. self._create_and_process(
  281. bulk_evaluator,
  282. {
  283. EventContentFields.MSC3952_MENTIONS: {
  284. "user_ids": [None, True, False, {}, [], self.alice]
  285. }
  286. },
  287. )
  288. )
  289. # The legacy push rule should not mention if the mentions field exists.
  290. self.assertFalse(
  291. self._create_and_process(
  292. bulk_evaluator,
  293. {
  294. "body": self.alice,
  295. "msgtype": "m.text",
  296. EventContentFields.MSC3952_MENTIONS: {},
  297. },
  298. )
  299. )
  300. @override_config(
  301. {
  302. "experimental_features": {
  303. "msc3952_intentional_mentions": True,
  304. "msc3966_exact_event_property_contains": True,
  305. }
  306. }
  307. )
  308. def test_room_mentions(self) -> None:
  309. """Test the behavior of an event which includes invalid room mentions."""
  310. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  311. # Room mentions from those without power should not notify.
  312. self.assertFalse(
  313. self._create_and_process(
  314. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  315. )
  316. )
  317. # Room mentions from those with power should notify.
  318. self.helper.send_state(
  319. self.room_id,
  320. "m.room.power_levels",
  321. {"notifications": {"room": 0}},
  322. self.token,
  323. state_key="",
  324. )
  325. self.assertTrue(
  326. self._create_and_process(
  327. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  328. )
  329. )
  330. # Invalid data should not notify.
  331. mentions: Any
  332. for mentions in (None, False, 1, "foo", [], {}):
  333. self.assertFalse(
  334. self._create_and_process(
  335. bulk_evaluator,
  336. {EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
  337. )
  338. )
  339. # The legacy push rule should not mention if the mentions field exists.
  340. self.assertFalse(
  341. self._create_and_process(
  342. bulk_evaluator,
  343. {
  344. "body": "@room",
  345. "msgtype": "m.text",
  346. EventContentFields.MSC3952_MENTIONS: {},
  347. },
  348. )
  349. )
  350. @override_config({"experimental_features": {"msc3958_supress_edit_notifs": True}})
  351. def test_suppress_edits(self) -> None:
  352. """Under the default push rules, event edits should not generate notifications."""
  353. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  354. # Create & persist an event to use as the parent of the relation.
  355. event, unpersisted_context = self.get_success(
  356. self.event_creation_handler.create_event(
  357. self.requester,
  358. {
  359. "type": "m.room.message",
  360. "room_id": self.room_id,
  361. "content": {
  362. "msgtype": "m.text",
  363. "body": "helo",
  364. },
  365. "sender": self.alice,
  366. },
  367. )
  368. )
  369. context = self.get_success(unpersisted_context.persist(event))
  370. self.get_success(
  371. self.event_creation_handler.handle_new_client_event(
  372. self.requester, events_and_context=[(event, context)]
  373. )
  374. )
  375. # Room mentions from those without power should not notify.
  376. self.assertFalse(
  377. self._create_and_process(
  378. bulk_evaluator,
  379. {
  380. "body": self.alice,
  381. "m.relates_to": {
  382. "rel_type": RelationTypes.REPLACE,
  383. "event_id": event.event_id,
  384. },
  385. },
  386. )
  387. )