test_utils.py 20 KB

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