test_push_rule_evaluator.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # Copyright 2020 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 Dict, Optional, Union
  15. import frozendict
  16. from synapse.api.room_versions import RoomVersions
  17. from synapse.events import FrozenEvent
  18. from synapse.push import push_rule_evaluator
  19. from synapse.push.push_rule_evaluator import PushRuleEvaluatorForEvent
  20. from synapse.types import JsonDict
  21. from tests import unittest
  22. class PushRuleEvaluatorTestCase(unittest.TestCase):
  23. def _get_evaluator(self, content: JsonDict) -> PushRuleEvaluatorForEvent:
  24. event = FrozenEvent(
  25. {
  26. "event_id": "$event_id",
  27. "type": "m.room.history_visibility",
  28. "sender": "@user:test",
  29. "state_key": "",
  30. "room_id": "#room:test",
  31. "content": content,
  32. },
  33. RoomVersions.V1,
  34. )
  35. room_member_count = 0
  36. sender_power_level = 0
  37. power_levels: Dict[str, Union[int, Dict[str, int]]] = {}
  38. return PushRuleEvaluatorForEvent(
  39. event, room_member_count, sender_power_level, power_levels
  40. )
  41. def test_display_name(self) -> None:
  42. """Check for a matching display name in the body of the event."""
  43. evaluator = self._get_evaluator({"body": "foo bar baz"})
  44. condition = {
  45. "kind": "contains_display_name",
  46. }
  47. # Blank names are skipped.
  48. self.assertFalse(evaluator.matches(condition, "@user:test", ""))
  49. # Check a display name that doesn't match.
  50. self.assertFalse(evaluator.matches(condition, "@user:test", "not found"))
  51. # Check a display name which matches.
  52. self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
  53. # A display name that matches, but not a full word does not result in a match.
  54. self.assertFalse(evaluator.matches(condition, "@user:test", "ba"))
  55. # A display name should not be interpreted as a regular expression.
  56. self.assertFalse(evaluator.matches(condition, "@user:test", "ba[rz]"))
  57. # A display name with spaces should work fine.
  58. self.assertTrue(evaluator.matches(condition, "@user:test", "foo bar"))
  59. def _assert_matches(
  60. self, condition: JsonDict, content: JsonDict, msg: Optional[str] = None
  61. ) -> None:
  62. evaluator = self._get_evaluator(content)
  63. self.assertTrue(evaluator.matches(condition, "@user:test", "display_name"), msg)
  64. def _assert_not_matches(
  65. self, condition: JsonDict, content: JsonDict, msg: Optional[str] = None
  66. ) -> None:
  67. evaluator = self._get_evaluator(content)
  68. self.assertFalse(
  69. evaluator.matches(condition, "@user:test", "display_name"), msg
  70. )
  71. def test_event_match_body(self) -> None:
  72. """Check that event_match conditions on content.body work as expected"""
  73. # if the key is `content.body`, the pattern matches substrings.
  74. # non-wildcards should match
  75. condition = {
  76. "kind": "event_match",
  77. "key": "content.body",
  78. "pattern": "foobaz",
  79. }
  80. self._assert_matches(
  81. condition,
  82. {"body": "aaa FoobaZ zzz"},
  83. "patterns should match and be case-insensitive",
  84. )
  85. self._assert_not_matches(
  86. condition,
  87. {"body": "aa xFoobaZ yy"},
  88. "pattern should only match at word boundaries",
  89. )
  90. self._assert_not_matches(
  91. condition,
  92. {"body": "aa foobazx yy"},
  93. "pattern should only match at word boundaries",
  94. )
  95. # wildcards should match
  96. condition = {
  97. "kind": "event_match",
  98. "key": "content.body",
  99. "pattern": "f?o*baz",
  100. }
  101. self._assert_matches(
  102. condition,
  103. {"body": "aaa FoobarbaZ zzz"},
  104. "* should match string and pattern should be case-insensitive",
  105. )
  106. self._assert_matches(
  107. condition, {"body": "aa foobaz yy"}, "* should match 0 characters"
  108. )
  109. self._assert_not_matches(
  110. condition, {"body": "aa fobbaz yy"}, "? should not match 0 characters"
  111. )
  112. self._assert_not_matches(
  113. condition, {"body": "aa fiiobaz yy"}, "? should not match 2 characters"
  114. )
  115. self._assert_not_matches(
  116. condition,
  117. {"body": "aa xfooxbaz yy"},
  118. "pattern should only match at word boundaries",
  119. )
  120. self._assert_not_matches(
  121. condition,
  122. {"body": "aa fooxbazx yy"},
  123. "pattern should only match at word boundaries",
  124. )
  125. # test backslashes
  126. condition = {
  127. "kind": "event_match",
  128. "key": "content.body",
  129. "pattern": r"f\oobaz",
  130. }
  131. self._assert_matches(
  132. condition,
  133. {"body": r"F\oobaz"},
  134. "backslash should match itself",
  135. )
  136. condition = {
  137. "kind": "event_match",
  138. "key": "content.body",
  139. "pattern": r"f\?obaz",
  140. }
  141. self._assert_matches(
  142. condition,
  143. {"body": r"F\oobaz"},
  144. r"? after \ should match any character",
  145. )
  146. def test_event_match_non_body(self) -> None:
  147. """Check that event_match conditions on other keys work as expected"""
  148. # if the key is anything other than 'content.body', the pattern must match the
  149. # whole value.
  150. # non-wildcards should match
  151. condition = {
  152. "kind": "event_match",
  153. "key": "content.value",
  154. "pattern": "foobaz",
  155. }
  156. self._assert_matches(
  157. condition,
  158. {"value": "FoobaZ"},
  159. "patterns should match and be case-insensitive",
  160. )
  161. self._assert_not_matches(
  162. condition,
  163. {"value": "xFoobaZ"},
  164. "pattern should only match at the start/end of the value",
  165. )
  166. self._assert_not_matches(
  167. condition,
  168. {"value": "FoobaZz"},
  169. "pattern should only match at the start/end of the value",
  170. )
  171. # it should work on frozendicts too
  172. self._assert_matches(
  173. condition,
  174. frozendict.frozendict({"value": "FoobaZ"}),
  175. "patterns should match on frozendicts",
  176. )
  177. # wildcards should match
  178. condition = {
  179. "kind": "event_match",
  180. "key": "content.value",
  181. "pattern": "f?o*baz",
  182. }
  183. self._assert_matches(
  184. condition,
  185. {"value": "FoobarbaZ"},
  186. "* should match string and pattern should be case-insensitive",
  187. )
  188. self._assert_matches(
  189. condition, {"value": "foobaz"}, "* should match 0 characters"
  190. )
  191. self._assert_not_matches(
  192. condition, {"value": "fobbaz"}, "? should not match 0 characters"
  193. )
  194. self._assert_not_matches(
  195. condition, {"value": "fiiobaz"}, "? should not match 2 characters"
  196. )
  197. self._assert_not_matches(
  198. condition,
  199. {"value": "xfooxbaz"},
  200. "pattern should only match at the start/end of the value",
  201. )
  202. self._assert_not_matches(
  203. condition,
  204. {"value": "fooxbazx"},
  205. "pattern should only match at the start/end of the value",
  206. )
  207. self._assert_not_matches(
  208. condition,
  209. {"value": "x\nfooxbaz"},
  210. "pattern should not match after a newline",
  211. )
  212. self._assert_not_matches(
  213. condition,
  214. {"value": "fooxbaz\nx"},
  215. "pattern should not match before a newline",
  216. )
  217. def test_no_body(self) -> None:
  218. """Not having a body shouldn't break the evaluator."""
  219. evaluator = self._get_evaluator({})
  220. condition = {
  221. "kind": "contains_display_name",
  222. }
  223. self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))
  224. def test_invalid_body(self) -> None:
  225. """A non-string body should not break the evaluator."""
  226. condition = {
  227. "kind": "contains_display_name",
  228. }
  229. for body in (1, True, {"foo": "bar"}):
  230. evaluator = self._get_evaluator({"body": body})
  231. self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))
  232. def test_tweaks_for_actions(self) -> None:
  233. """
  234. This tests the behaviour of tweaks_for_actions.
  235. """
  236. actions = [
  237. {"set_tweak": "sound", "value": "default"},
  238. {"set_tweak": "highlight"},
  239. "notify",
  240. ]
  241. self.assertEqual(
  242. push_rule_evaluator.tweaks_for_actions(actions),
  243. {"sound": "default", "highlight": True},
  244. )