test_utils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. # Copyright 2015, 2016 OpenMarket Ltd
  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 synapse.api.constants import EventContentFields
  15. from synapse.api.room_versions import RoomVersions
  16. from synapse.events import make_event_from_dict
  17. from synapse.events.utils import (
  18. SerializeEventConfig,
  19. copy_and_fixup_power_levels_contents,
  20. prune_event,
  21. serialize_event,
  22. )
  23. from synapse.util.frozenutils import freeze
  24. from tests import unittest
  25. def MockEvent(**kwargs):
  26. if "event_id" not in kwargs:
  27. kwargs["event_id"] = "fake_event_id"
  28. if "type" not in kwargs:
  29. kwargs["type"] = "fake_type"
  30. return make_event_from_dict(kwargs)
  31. class PruneEventTestCase(unittest.TestCase):
  32. def run_test(self, evdict, matchdict, **kwargs):
  33. """
  34. Asserts that a new event constructed with `evdict` will look like
  35. `matchdict` when it is redacted.
  36. Args:
  37. evdict: The dictionary to build the event from.
  38. matchdict: The expected resulting dictionary.
  39. kwargs: Additional keyword arguments used to create the event.
  40. """
  41. self.assertEqual(
  42. prune_event(make_event_from_dict(evdict, **kwargs)).get_dict(), matchdict
  43. )
  44. def test_minimal(self):
  45. self.run_test(
  46. {"type": "A", "event_id": "$test:domain"},
  47. {
  48. "type": "A",
  49. "event_id": "$test:domain",
  50. "content": {},
  51. "signatures": {},
  52. "unsigned": {},
  53. },
  54. )
  55. def test_basic_keys(self):
  56. """Ensure that the keys that should be untouched are kept."""
  57. # Note that some of the values below don't really make sense, but the
  58. # pruning of events doesn't worry about the values of any fields (with
  59. # the exception of the content field).
  60. self.run_test(
  61. {
  62. "event_id": "$3:domain",
  63. "type": "A",
  64. "room_id": "!1:domain",
  65. "sender": "@2:domain",
  66. "state_key": "B",
  67. "content": {"other_key": "foo"},
  68. "hashes": "hashes",
  69. "signatures": {"domain": {"algo:1": "sigs"}},
  70. "depth": 4,
  71. "prev_events": "prev_events",
  72. "prev_state": "prev_state",
  73. "auth_events": "auth_events",
  74. "origin": "domain",
  75. "origin_server_ts": 1234,
  76. "membership": "join",
  77. # Also include a key that should be removed.
  78. "other_key": "foo",
  79. },
  80. {
  81. "event_id": "$3:domain",
  82. "type": "A",
  83. "room_id": "!1:domain",
  84. "sender": "@2:domain",
  85. "state_key": "B",
  86. "hashes": "hashes",
  87. "depth": 4,
  88. "prev_events": "prev_events",
  89. "prev_state": "prev_state",
  90. "auth_events": "auth_events",
  91. "origin": "domain",
  92. "origin_server_ts": 1234,
  93. "membership": "join",
  94. "content": {},
  95. "signatures": {"domain": {"algo:1": "sigs"}},
  96. "unsigned": {},
  97. },
  98. )
  99. # As of MSC2176 we now redact the membership and prev_states keys.
  100. self.run_test(
  101. {"type": "A", "prev_state": "prev_state", "membership": "join"},
  102. {"type": "A", "content": {}, "signatures": {}, "unsigned": {}},
  103. room_version=RoomVersions.MSC2176,
  104. )
  105. def test_unsigned(self):
  106. """Ensure that unsigned properties get stripped (except age_ts and replaces_state)."""
  107. self.run_test(
  108. {
  109. "type": "B",
  110. "event_id": "$test:domain",
  111. "unsigned": {
  112. "age_ts": 20,
  113. "replaces_state": "$test2:domain",
  114. "other_key": "foo",
  115. },
  116. },
  117. {
  118. "type": "B",
  119. "event_id": "$test:domain",
  120. "content": {},
  121. "signatures": {},
  122. "unsigned": {"age_ts": 20, "replaces_state": "$test2:domain"},
  123. },
  124. )
  125. def test_content(self):
  126. """The content dictionary should be stripped in most cases."""
  127. self.run_test(
  128. {"type": "C", "event_id": "$test:domain", "content": {"things": "here"}},
  129. {
  130. "type": "C",
  131. "event_id": "$test:domain",
  132. "content": {},
  133. "signatures": {},
  134. "unsigned": {},
  135. },
  136. )
  137. # Some events keep a single content key/value.
  138. EVENT_KEEP_CONTENT_KEYS = [
  139. ("member", "membership", "join"),
  140. ("join_rules", "join_rule", "invite"),
  141. ("history_visibility", "history_visibility", "shared"),
  142. ]
  143. for event_type, key, value in EVENT_KEEP_CONTENT_KEYS:
  144. self.run_test(
  145. {
  146. "type": "m.room." + event_type,
  147. "event_id": "$test:domain",
  148. "content": {key: value, "other_key": "foo"},
  149. },
  150. {
  151. "type": "m.room." + event_type,
  152. "event_id": "$test:domain",
  153. "content": {key: value},
  154. "signatures": {},
  155. "unsigned": {},
  156. },
  157. )
  158. def test_create(self):
  159. """Create events are partially redacted until MSC2176."""
  160. self.run_test(
  161. {
  162. "type": "m.room.create",
  163. "event_id": "$test:domain",
  164. "content": {"creator": "@2:domain", "other_key": "foo"},
  165. },
  166. {
  167. "type": "m.room.create",
  168. "event_id": "$test:domain",
  169. "content": {"creator": "@2:domain"},
  170. "signatures": {},
  171. "unsigned": {},
  172. },
  173. )
  174. # After MSC2176, create events get nothing redacted.
  175. self.run_test(
  176. {"type": "m.room.create", "content": {"not_a_real_key": True}},
  177. {
  178. "type": "m.room.create",
  179. "content": {"not_a_real_key": True},
  180. "signatures": {},
  181. "unsigned": {},
  182. },
  183. room_version=RoomVersions.MSC2176,
  184. )
  185. def test_power_levels(self):
  186. """Power level events keep a variety of content keys."""
  187. self.run_test(
  188. {
  189. "type": "m.room.power_levels",
  190. "event_id": "$test:domain",
  191. "content": {
  192. "ban": 1,
  193. "events": {"m.room.name": 100},
  194. "events_default": 2,
  195. "invite": 3,
  196. "kick": 4,
  197. "redact": 5,
  198. "state_default": 6,
  199. "users": {"@admin:domain": 100},
  200. "users_default": 7,
  201. "other_key": 8,
  202. },
  203. },
  204. {
  205. "type": "m.room.power_levels",
  206. "event_id": "$test:domain",
  207. "content": {
  208. "ban": 1,
  209. "events": {"m.room.name": 100},
  210. "events_default": 2,
  211. # Note that invite is not here.
  212. "kick": 4,
  213. "redact": 5,
  214. "state_default": 6,
  215. "users": {"@admin:domain": 100},
  216. "users_default": 7,
  217. },
  218. "signatures": {},
  219. "unsigned": {},
  220. },
  221. )
  222. # After MSC2176, power levels events keep the invite key.
  223. self.run_test(
  224. {"type": "m.room.power_levels", "content": {"invite": 75}},
  225. {
  226. "type": "m.room.power_levels",
  227. "content": {"invite": 75},
  228. "signatures": {},
  229. "unsigned": {},
  230. },
  231. room_version=RoomVersions.MSC2176,
  232. )
  233. def test_alias_event(self):
  234. """Alias events have special behavior up through room version 6."""
  235. self.run_test(
  236. {
  237. "type": "m.room.aliases",
  238. "event_id": "$test:domain",
  239. "content": {"aliases": ["test"]},
  240. },
  241. {
  242. "type": "m.room.aliases",
  243. "event_id": "$test:domain",
  244. "content": {"aliases": ["test"]},
  245. "signatures": {},
  246. "unsigned": {},
  247. },
  248. )
  249. # After MSC2432, alias events have no special behavior.
  250. self.run_test(
  251. {"type": "m.room.aliases", "content": {"aliases": ["test"]}},
  252. {
  253. "type": "m.room.aliases",
  254. "content": {},
  255. "signatures": {},
  256. "unsigned": {},
  257. },
  258. room_version=RoomVersions.V6,
  259. )
  260. def test_redacts(self):
  261. """Redaction events have no special behaviour until MSC2174/MSC2176."""
  262. self.run_test(
  263. {"type": "m.room.redaction", "content": {"redacts": "$test2:domain"}},
  264. {
  265. "type": "m.room.redaction",
  266. "content": {},
  267. "signatures": {},
  268. "unsigned": {},
  269. },
  270. room_version=RoomVersions.V6,
  271. )
  272. # After MSC2174, redaction events keep the redacts content key.
  273. self.run_test(
  274. {"type": "m.room.redaction", "content": {"redacts": "$test2:domain"}},
  275. {
  276. "type": "m.room.redaction",
  277. "content": {"redacts": "$test2:domain"},
  278. "signatures": {},
  279. "unsigned": {},
  280. },
  281. room_version=RoomVersions.MSC2176,
  282. )
  283. def test_join_rules(self):
  284. """Join rules events have changed behavior starting with MSC3083."""
  285. self.run_test(
  286. {
  287. "type": "m.room.join_rules",
  288. "event_id": "$test:domain",
  289. "content": {
  290. "join_rule": "invite",
  291. "allow": [],
  292. "other_key": "stripped",
  293. },
  294. },
  295. {
  296. "type": "m.room.join_rules",
  297. "event_id": "$test:domain",
  298. "content": {"join_rule": "invite"},
  299. "signatures": {},
  300. "unsigned": {},
  301. },
  302. )
  303. # After MSC3083, the allow key is protected from redaction.
  304. self.run_test(
  305. {
  306. "type": "m.room.join_rules",
  307. "content": {
  308. "join_rule": "invite",
  309. "allow": [],
  310. "other_key": "stripped",
  311. },
  312. },
  313. {
  314. "type": "m.room.join_rules",
  315. "content": {
  316. "join_rule": "invite",
  317. "allow": [],
  318. },
  319. "signatures": {},
  320. "unsigned": {},
  321. },
  322. room_version=RoomVersions.V8,
  323. )
  324. def test_member(self):
  325. """Member events have changed behavior starting with MSC3375."""
  326. self.run_test(
  327. {
  328. "type": "m.room.member",
  329. "event_id": "$test:domain",
  330. "content": {
  331. "membership": "join",
  332. EventContentFields.AUTHORISING_USER: "@user:domain",
  333. "other_key": "stripped",
  334. },
  335. },
  336. {
  337. "type": "m.room.member",
  338. "event_id": "$test:domain",
  339. "content": {"membership": "join"},
  340. "signatures": {},
  341. "unsigned": {},
  342. },
  343. )
  344. # After MSC3375, the join_authorised_via_users_server key is protected
  345. # from redaction.
  346. self.run_test(
  347. {
  348. "type": "m.room.member",
  349. "content": {
  350. "membership": "join",
  351. EventContentFields.AUTHORISING_USER: "@user:domain",
  352. "other_key": "stripped",
  353. },
  354. },
  355. {
  356. "type": "m.room.member",
  357. "content": {
  358. "membership": "join",
  359. EventContentFields.AUTHORISING_USER: "@user:domain",
  360. },
  361. "signatures": {},
  362. "unsigned": {},
  363. },
  364. room_version=RoomVersions.V9,
  365. )
  366. class SerializeEventTestCase(unittest.TestCase):
  367. def serialize(self, ev, fields):
  368. return serialize_event(
  369. ev, 1479807801915, config=SerializeEventConfig(only_event_fields=fields)
  370. )
  371. def test_event_fields_works_with_keys(self):
  372. self.assertEqual(
  373. self.serialize(
  374. MockEvent(sender="@alice:localhost", room_id="!foo:bar"), ["room_id"]
  375. ),
  376. {"room_id": "!foo:bar"},
  377. )
  378. def test_event_fields_works_with_nested_keys(self):
  379. self.assertEqual(
  380. self.serialize(
  381. MockEvent(
  382. sender="@alice:localhost",
  383. room_id="!foo:bar",
  384. content={"body": "A message"},
  385. ),
  386. ["content.body"],
  387. ),
  388. {"content": {"body": "A message"}},
  389. )
  390. def test_event_fields_works_with_dot_keys(self):
  391. self.assertEqual(
  392. self.serialize(
  393. MockEvent(
  394. sender="@alice:localhost",
  395. room_id="!foo:bar",
  396. content={"key.with.dots": {}},
  397. ),
  398. [r"content.key\.with\.dots"],
  399. ),
  400. {"content": {"key.with.dots": {}}},
  401. )
  402. def test_event_fields_works_with_nested_dot_keys(self):
  403. self.assertEqual(
  404. self.serialize(
  405. MockEvent(
  406. sender="@alice:localhost",
  407. room_id="!foo:bar",
  408. content={
  409. "not_me": 1,
  410. "nested.dot.key": {"leaf.key": 42, "not_me_either": 1},
  411. },
  412. ),
  413. [r"content.nested\.dot\.key.leaf\.key"],
  414. ),
  415. {"content": {"nested.dot.key": {"leaf.key": 42}}},
  416. )
  417. def test_event_fields_nops_with_unknown_keys(self):
  418. self.assertEqual(
  419. self.serialize(
  420. MockEvent(
  421. sender="@alice:localhost",
  422. room_id="!foo:bar",
  423. content={"foo": "bar"},
  424. ),
  425. ["content.foo", "content.notexists"],
  426. ),
  427. {"content": {"foo": "bar"}},
  428. )
  429. def test_event_fields_nops_with_non_dict_keys(self):
  430. self.assertEqual(
  431. self.serialize(
  432. MockEvent(
  433. sender="@alice:localhost",
  434. room_id="!foo:bar",
  435. content={"foo": ["I", "am", "an", "array"]},
  436. ),
  437. ["content.foo.am"],
  438. ),
  439. {},
  440. )
  441. def test_event_fields_nops_with_array_keys(self):
  442. self.assertEqual(
  443. self.serialize(
  444. MockEvent(
  445. sender="@alice:localhost",
  446. room_id="!foo:bar",
  447. content={"foo": ["I", "am", "an", "array"]},
  448. ),
  449. ["content.foo.1"],
  450. ),
  451. {},
  452. )
  453. def test_event_fields_all_fields_if_empty(self):
  454. self.assertEqual(
  455. self.serialize(
  456. MockEvent(
  457. type="foo",
  458. event_id="test",
  459. room_id="!foo:bar",
  460. content={"foo": "bar"},
  461. ),
  462. [],
  463. ),
  464. {
  465. "type": "foo",
  466. "event_id": "test",
  467. "room_id": "!foo:bar",
  468. "content": {"foo": "bar"},
  469. "unsigned": {},
  470. },
  471. )
  472. def test_event_fields_fail_if_fields_not_str(self):
  473. with self.assertRaises(TypeError):
  474. self.serialize(
  475. MockEvent(room_id="!foo:bar", content={"foo": "bar"}), ["room_id", 4]
  476. )
  477. class CopyPowerLevelsContentTestCase(unittest.TestCase):
  478. def setUp(self) -> None:
  479. self.test_content = {
  480. "ban": 50,
  481. "events": {"m.room.name": 100, "m.room.power_levels": 100},
  482. "events_default": 0,
  483. "invite": 50,
  484. "kick": 50,
  485. "notifications": {"room": 20},
  486. "redact": 50,
  487. "state_default": 50,
  488. "users": {"@example:localhost": 100},
  489. "users_default": 0,
  490. }
  491. def _test(self, input):
  492. a = copy_and_fixup_power_levels_contents(input)
  493. self.assertEqual(a["ban"], 50)
  494. self.assertEqual(a["events"]["m.room.name"], 100)
  495. # make sure that changing the copy changes the copy and not the orig
  496. a["ban"] = 10
  497. a["events"]["m.room.power_levels"] = 20
  498. self.assertEqual(input["ban"], 50)
  499. self.assertEqual(input["events"]["m.room.power_levels"], 100)
  500. def test_unfrozen(self):
  501. self._test(self.test_content)
  502. def test_frozen(self):
  503. input = freeze(self.test_content)
  504. self._test(input)
  505. def test_stringy_integers(self):
  506. """String representations of decimal integers are converted to integers."""
  507. input = {
  508. "a": "100",
  509. "b": {
  510. "foo": 99,
  511. "bar": "-98",
  512. },
  513. "d": "0999",
  514. }
  515. output = copy_and_fixup_power_levels_contents(input)
  516. expected_output = {
  517. "a": 100,
  518. "b": {
  519. "foo": 99,
  520. "bar": -98,
  521. },
  522. "d": 999,
  523. }
  524. self.assertEqual(output, expected_output)
  525. def test_strings_that_dont_represent_decimal_integers(self) -> None:
  526. """Should raise when given inputs `s` for which `int(s, base=10)` raises."""
  527. for invalid_string in ["0x123", "123.0", "123.45", "hello", "0b1", "0o777"]:
  528. with self.assertRaises(TypeError):
  529. copy_and_fixup_power_levels_contents({"a": invalid_string})
  530. def test_invalid_types_raise_type_error(self) -> None:
  531. with self.assertRaises(TypeError):
  532. copy_and_fixup_power_levels_contents({"a": ["hello", "grandma"]}) # type: ignore[arg-type]
  533. copy_and_fixup_power_levels_contents({"a": None}) # type: ignore[arg-type]
  534. def test_invalid_nesting_raises_type_error(self) -> None:
  535. with self.assertRaises(TypeError):
  536. copy_and_fixup_power_levels_contents({"a": {"b": {"c": 1}}})