You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

839 lines
28 KiB

  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 typing import Any, List, Mapping, Optional
  16. import attr
  17. from parameterized import parameterized
  18. from synapse.api.constants import EventContentFields
  19. from synapse.api.room_versions import RoomVersions
  20. from synapse.events import EventBase, make_event_from_dict
  21. from synapse.events.utils import (
  22. PowerLevelsContent,
  23. SerializeEventConfig,
  24. _split_field,
  25. copy_and_fixup_power_levels_contents,
  26. maybe_upsert_event_field,
  27. prune_event,
  28. serialize_event,
  29. )
  30. from synapse.types import JsonDict
  31. from synapse.util.frozenutils import freeze
  32. def MockEvent(**kwargs: Any) -> EventBase:
  33. if "event_id" not in kwargs:
  34. kwargs["event_id"] = "fake_event_id"
  35. if "type" not in kwargs:
  36. kwargs["type"] = "fake_type"
  37. if "content" not in kwargs:
  38. kwargs["content"] = {}
  39. return make_event_from_dict(kwargs)
  40. class TestMaybeUpsertEventField(stdlib_unittest.TestCase):
  41. def test_update_okay(self) -> None:
  42. event = make_event_from_dict({"event_id": "$1234"})
  43. success = maybe_upsert_event_field(event, event.unsigned, "key", "value")
  44. self.assertTrue(success)
  45. self.assertEqual(event.unsigned["key"], "value")
  46. def test_update_not_okay(self) -> None:
  47. event = make_event_from_dict({"event_id": "$1234"})
  48. LARGE_STRING = "a" * 100_000
  49. success = maybe_upsert_event_field(event, event.unsigned, "key", LARGE_STRING)
  50. self.assertFalse(success)
  51. self.assertNotIn("key", event.unsigned)
  52. def test_update_not_okay_leaves_original_value(self) -> None:
  53. event = make_event_from_dict(
  54. {"event_id": "$1234", "unsigned": {"key": "value"}}
  55. )
  56. LARGE_STRING = "a" * 100_000
  57. success = maybe_upsert_event_field(event, event.unsigned, "key", LARGE_STRING)
  58. self.assertFalse(success)
  59. self.assertEqual(event.unsigned["key"], "value")
  60. class PruneEventTestCase(stdlib_unittest.TestCase):
  61. def run_test(self, evdict: JsonDict, matchdict: JsonDict, **kwargs: Any) -> None:
  62. """
  63. Asserts that a new event constructed with `evdict` will look like
  64. `matchdict` when it is redacted.
  65. Args:
  66. evdict: The dictionary to build the event from.
  67. matchdict: The expected resulting dictionary.
  68. kwargs: Additional keyword arguments used to create the event.
  69. """
  70. self.assertEqual(
  71. prune_event(make_event_from_dict(evdict, **kwargs)).get_dict(), matchdict
  72. )
  73. def test_minimal(self) -> None:
  74. self.run_test(
  75. {"type": "A", "event_id": "$test:domain"},
  76. {
  77. "type": "A",
  78. "event_id": "$test:domain",
  79. "content": {},
  80. "signatures": {},
  81. "unsigned": {},
  82. },
  83. )
  84. def test_basic_keys(self) -> None:
  85. """Ensure that the keys that should be untouched are kept."""
  86. # Note that some of the values below don't really make sense, but the
  87. # pruning of events doesn't worry about the values of any fields (with
  88. # the exception of the content field).
  89. self.run_test(
  90. {
  91. "event_id": "$3:domain",
  92. "type": "A",
  93. "room_id": "!1:domain",
  94. "sender": "@2:domain",
  95. "state_key": "B",
  96. "content": {"other_key": "foo"},
  97. "hashes": "hashes",
  98. "signatures": {"domain": {"algo:1": "sigs"}},
  99. "depth": 4,
  100. "prev_events": "prev_events",
  101. "prev_state": "prev_state",
  102. "auth_events": "auth_events",
  103. "origin": "domain",
  104. "origin_server_ts": 1234,
  105. "membership": "join",
  106. # Also include a key that should be removed.
  107. "other_key": "foo",
  108. },
  109. {
  110. "event_id": "$3:domain",
  111. "type": "A",
  112. "room_id": "!1:domain",
  113. "sender": "@2:domain",
  114. "state_key": "B",
  115. "hashes": "hashes",
  116. "depth": 4,
  117. "prev_events": "prev_events",
  118. "prev_state": "prev_state",
  119. "auth_events": "auth_events",
  120. "origin": "domain",
  121. "origin_server_ts": 1234,
  122. "membership": "join",
  123. "content": {},
  124. "signatures": {"domain": {"algo:1": "sigs"}},
  125. "unsigned": {},
  126. },
  127. )
  128. # As of room versions we now redact the membership, prev_states, and origin keys.
  129. self.run_test(
  130. {
  131. "type": "A",
  132. "prev_state": "prev_state",
  133. "membership": "join",
  134. "origin": "example.com",
  135. },
  136. {"type": "A", "content": {}, "signatures": {}, "unsigned": {}},
  137. room_version=RoomVersions.V11,
  138. )
  139. def test_unsigned(self) -> None:
  140. """Ensure that unsigned properties get stripped (except age_ts and replaces_state)."""
  141. self.run_test(
  142. {
  143. "type": "B",
  144. "event_id": "$test:domain",
  145. "unsigned": {
  146. "age_ts": 20,
  147. "replaces_state": "$test2:domain",
  148. "other_key": "foo",
  149. },
  150. },
  151. {
  152. "type": "B",
  153. "event_id": "$test:domain",
  154. "content": {},
  155. "signatures": {},
  156. "unsigned": {"age_ts": 20, "replaces_state": "$test2:domain"},
  157. },
  158. )
  159. def test_content(self) -> None:
  160. """The content dictionary should be stripped in most cases."""
  161. self.run_test(
  162. {"type": "C", "event_id": "$test:domain", "content": {"things": "here"}},
  163. {
  164. "type": "C",
  165. "event_id": "$test:domain",
  166. "content": {},
  167. "signatures": {},
  168. "unsigned": {},
  169. },
  170. )
  171. # Some events keep a single content key/value.
  172. EVENT_KEEP_CONTENT_KEYS = [
  173. ("member", "membership", "join"),
  174. ("join_rules", "join_rule", "invite"),
  175. ("history_visibility", "history_visibility", "shared"),
  176. ]
  177. for event_type, key, value in EVENT_KEEP_CONTENT_KEYS:
  178. self.run_test(
  179. {
  180. "type": "m.room." + event_type,
  181. "event_id": "$test:domain",
  182. "content": {key: value, "other_key": "foo"},
  183. },
  184. {
  185. "type": "m.room." + event_type,
  186. "event_id": "$test:domain",
  187. "content": {key: value},
  188. "signatures": {},
  189. "unsigned": {},
  190. },
  191. )
  192. def test_create(self) -> None:
  193. """Create events are partially redacted until MSC2176."""
  194. self.run_test(
  195. {
  196. "type": "m.room.create",
  197. "event_id": "$test:domain",
  198. "content": {"creator": "@2:domain", "other_key": "foo"},
  199. },
  200. {
  201. "type": "m.room.create",
  202. "event_id": "$test:domain",
  203. "content": {"creator": "@2:domain"},
  204. "signatures": {},
  205. "unsigned": {},
  206. },
  207. )
  208. # After MSC2176, create events should preserve field `content`
  209. self.run_test(
  210. {
  211. "type": "m.room.create",
  212. "content": {"not_a_real_key": True},
  213. "origin": "some_homeserver",
  214. "nonsense_field": "some_random_garbage",
  215. },
  216. {
  217. "type": "m.room.create",
  218. "content": {"not_a_real_key": True},
  219. "signatures": {},
  220. "unsigned": {},
  221. },
  222. room_version=RoomVersions.V11,
  223. )
  224. def test_power_levels(self) -> None:
  225. """Power level events keep a variety of content keys."""
  226. self.run_test(
  227. {
  228. "type": "m.room.power_levels",
  229. "event_id": "$test:domain",
  230. "content": {
  231. "ban": 1,
  232. "events": {"m.room.name": 100},
  233. "events_default": 2,
  234. "invite": 3,
  235. "kick": 4,
  236. "redact": 5,
  237. "state_default": 6,
  238. "users": {"@admin:domain": 100},
  239. "users_default": 7,
  240. "other_key": 8,
  241. },
  242. },
  243. {
  244. "type": "m.room.power_levels",
  245. "event_id": "$test:domain",
  246. "content": {
  247. "ban": 1,
  248. "events": {"m.room.name": 100},
  249. "events_default": 2,
  250. # Note that invite is not here.
  251. "kick": 4,
  252. "redact": 5,
  253. "state_default": 6,
  254. "users": {"@admin:domain": 100},
  255. "users_default": 7,
  256. },
  257. "signatures": {},
  258. "unsigned": {},
  259. },
  260. )
  261. # After MSC2176, power levels events keep the invite key.
  262. self.run_test(
  263. {"type": "m.room.power_levels", "content": {"invite": 75}},
  264. {
  265. "type": "m.room.power_levels",
  266. "content": {"invite": 75},
  267. "signatures": {},
  268. "unsigned": {},
  269. },
  270. room_version=RoomVersions.V11,
  271. )
  272. def test_alias_event(self) -> None:
  273. """Alias events have special behavior up through room version 6."""
  274. self.run_test(
  275. {
  276. "type": "m.room.aliases",
  277. "event_id": "$test:domain",
  278. "content": {"aliases": ["test"]},
  279. },
  280. {
  281. "type": "m.room.aliases",
  282. "event_id": "$test:domain",
  283. "content": {"aliases": ["test"]},
  284. "signatures": {},
  285. "unsigned": {},
  286. },
  287. )
  288. # After MSC2432, alias events have no special behavior.
  289. self.run_test(
  290. {"type": "m.room.aliases", "content": {"aliases": ["test"]}},
  291. {
  292. "type": "m.room.aliases",
  293. "content": {},
  294. "signatures": {},
  295. "unsigned": {},
  296. },
  297. room_version=RoomVersions.V6,
  298. )
  299. def test_redacts(self) -> None:
  300. """Redaction events have no special behaviour until MSC2174/MSC2176."""
  301. self.run_test(
  302. {
  303. "type": "m.room.redaction",
  304. "content": {"redacts": "$test2:domain"},
  305. "redacts": "$test2:domain",
  306. },
  307. {
  308. "type": "m.room.redaction",
  309. "content": {},
  310. "signatures": {},
  311. "unsigned": {},
  312. },
  313. room_version=RoomVersions.V6,
  314. )
  315. # After MSC2174, redaction events keep the redacts content key.
  316. self.run_test(
  317. {
  318. "type": "m.room.redaction",
  319. "content": {"redacts": "$test2:domain"},
  320. "redacts": "$test2:domain",
  321. },
  322. {
  323. "type": "m.room.redaction",
  324. "content": {"redacts": "$test2:domain"},
  325. "signatures": {},
  326. "unsigned": {},
  327. },
  328. room_version=RoomVersions.V11,
  329. )
  330. def test_join_rules(self) -> None:
  331. """Join rules events have changed behavior starting with MSC3083."""
  332. self.run_test(
  333. {
  334. "type": "m.room.join_rules",
  335. "event_id": "$test:domain",
  336. "content": {
  337. "join_rule": "invite",
  338. "allow": [],
  339. "other_key": "stripped",
  340. },
  341. },
  342. {
  343. "type": "m.room.join_rules",
  344. "event_id": "$test:domain",
  345. "content": {"join_rule": "invite"},
  346. "signatures": {},
  347. "unsigned": {},
  348. },
  349. )
  350. # After MSC3083, the allow key is protected from redaction.
  351. self.run_test(
  352. {
  353. "type": "m.room.join_rules",
  354. "content": {
  355. "join_rule": "invite",
  356. "allow": [],
  357. "other_key": "stripped",
  358. },
  359. },
  360. {
  361. "type": "m.room.join_rules",
  362. "content": {
  363. "join_rule": "invite",
  364. "allow": [],
  365. },
  366. "signatures": {},
  367. "unsigned": {},
  368. },
  369. room_version=RoomVersions.V8,
  370. )
  371. def test_member(self) -> None:
  372. """Member events have changed behavior in MSC3375 and MSC3821."""
  373. self.run_test(
  374. {
  375. "type": "m.room.member",
  376. "event_id": "$test:domain",
  377. "content": {
  378. "membership": "join",
  379. EventContentFields.AUTHORISING_USER: "@user:domain",
  380. "other_key": "stripped",
  381. },
  382. },
  383. {
  384. "type": "m.room.member",
  385. "event_id": "$test:domain",
  386. "content": {"membership": "join"},
  387. "signatures": {},
  388. "unsigned": {},
  389. },
  390. )
  391. # After MSC3375, the join_authorised_via_users_server key is protected
  392. # from redaction.
  393. self.run_test(
  394. {
  395. "type": "m.room.member",
  396. "content": {
  397. "membership": "join",
  398. EventContentFields.AUTHORISING_USER: "@user:domain",
  399. "other_key": "stripped",
  400. },
  401. },
  402. {
  403. "type": "m.room.member",
  404. "content": {
  405. "membership": "join",
  406. EventContentFields.AUTHORISING_USER: "@user:domain",
  407. },
  408. "signatures": {},
  409. "unsigned": {},
  410. },
  411. room_version=RoomVersions.V9,
  412. )
  413. # After MSC3821, the signed key under third_party_invite is protected
  414. # from redaction.
  415. THIRD_PARTY_INVITE = {
  416. "display_name": "alice",
  417. "signed": {
  418. "mxid": "@alice:example.org",
  419. "signatures": {
  420. "magic.forest": {
  421. "ed25519:3": "fQpGIW1Snz+pwLZu6sTy2aHy/DYWWTspTJRPyNp0PKkymfIsNffysMl6ObMMFdIJhk6g6pwlIqZ54rxo8SLmAg"
  422. }
  423. },
  424. "token": "abc123",
  425. },
  426. }
  427. self.run_test(
  428. {
  429. "type": "m.room.member",
  430. "content": {
  431. "membership": "invite",
  432. "third_party_invite": THIRD_PARTY_INVITE,
  433. "other_key": "stripped",
  434. },
  435. },
  436. {
  437. "type": "m.room.member",
  438. "content": {
  439. "membership": "invite",
  440. "third_party_invite": {"signed": THIRD_PARTY_INVITE["signed"]},
  441. },
  442. "signatures": {},
  443. "unsigned": {},
  444. },
  445. room_version=RoomVersions.V11,
  446. )
  447. # Ensure this doesn't break if an invalid field is sent.
  448. self.run_test(
  449. {
  450. "type": "m.room.member",
  451. "content": {
  452. "membership": "invite",
  453. "third_party_invite": {},
  454. "other_key": "stripped",
  455. },
  456. },
  457. {
  458. "type": "m.room.member",
  459. "content": {"membership": "invite", "third_party_invite": {}},
  460. "signatures": {},
  461. "unsigned": {},
  462. },
  463. room_version=RoomVersions.V11,
  464. )
  465. self.run_test(
  466. {
  467. "type": "m.room.member",
  468. "content": {
  469. "membership": "invite",
  470. "third_party_invite": "stripped",
  471. "other_key": "stripped",
  472. },
  473. },
  474. {
  475. "type": "m.room.member",
  476. "content": {"membership": "invite"},
  477. "signatures": {},
  478. "unsigned": {},
  479. },
  480. room_version=RoomVersions.V11,
  481. )
  482. def test_relations(self) -> None:
  483. """Event relations get redacted until MSC3389."""
  484. # Normally the m._relates_to field is redacted.
  485. self.run_test(
  486. {
  487. "type": "m.room.message",
  488. "content": {
  489. "body": "foo",
  490. "m.relates_to": {
  491. "rel_type": "rel_type",
  492. "event_id": "$parent:domain",
  493. "other": "stripped",
  494. },
  495. },
  496. },
  497. {
  498. "type": "m.room.message",
  499. "content": {},
  500. "signatures": {},
  501. "unsigned": {},
  502. },
  503. room_version=RoomVersions.V10,
  504. )
  505. # Create a new room version.
  506. msc3389_room_ver = attr.evolve(
  507. RoomVersions.V10, msc3389_relation_redactions=True
  508. )
  509. self.run_test(
  510. {
  511. "type": "m.room.message",
  512. "content": {
  513. "body": "foo",
  514. "m.relates_to": {
  515. "rel_type": "rel_type",
  516. "event_id": "$parent:domain",
  517. "other": "stripped",
  518. },
  519. },
  520. },
  521. {
  522. "type": "m.room.message",
  523. "content": {
  524. "m.relates_to": {
  525. "rel_type": "rel_type",
  526. "event_id": "$parent:domain",
  527. },
  528. },
  529. "signatures": {},
  530. "unsigned": {},
  531. },
  532. room_version=msc3389_room_ver,
  533. )
  534. # If the field is not an object, redact it.
  535. self.run_test(
  536. {
  537. "type": "m.room.message",
  538. "content": {
  539. "body": "foo",
  540. "m.relates_to": "stripped",
  541. },
  542. },
  543. {
  544. "type": "m.room.message",
  545. "content": {},
  546. "signatures": {},
  547. "unsigned": {},
  548. },
  549. room_version=msc3389_room_ver,
  550. )
  551. # If the m.relates_to property would be empty, redact it.
  552. self.run_test(
  553. {
  554. "type": "m.room.message",
  555. "content": {"body": "foo", "m.relates_to": {"foo": "stripped"}},
  556. },
  557. {
  558. "type": "m.room.message",
  559. "content": {},
  560. "signatures": {},
  561. "unsigned": {},
  562. },
  563. room_version=msc3389_room_ver,
  564. )
  565. class SerializeEventTestCase(stdlib_unittest.TestCase):
  566. def serialize(self, ev: EventBase, fields: Optional[List[str]]) -> JsonDict:
  567. return serialize_event(
  568. ev, 1479807801915, config=SerializeEventConfig(only_event_fields=fields)
  569. )
  570. def test_event_fields_works_with_keys(self) -> None:
  571. self.assertEqual(
  572. self.serialize(
  573. MockEvent(sender="@alice:localhost", room_id="!foo:bar"), ["room_id"]
  574. ),
  575. {"room_id": "!foo:bar"},
  576. )
  577. def test_event_fields_works_with_nested_keys(self) -> None:
  578. self.assertEqual(
  579. self.serialize(
  580. MockEvent(
  581. sender="@alice:localhost",
  582. room_id="!foo:bar",
  583. content={"body": "A message"},
  584. ),
  585. ["content.body"],
  586. ),
  587. {"content": {"body": "A message"}},
  588. )
  589. def test_event_fields_works_with_dot_keys(self) -> None:
  590. self.assertEqual(
  591. self.serialize(
  592. MockEvent(
  593. sender="@alice:localhost",
  594. room_id="!foo:bar",
  595. content={"key.with.dots": {}},
  596. ),
  597. [r"content.key\.with\.dots"],
  598. ),
  599. {"content": {"key.with.dots": {}}},
  600. )
  601. def test_event_fields_works_with_nested_dot_keys(self) -> None:
  602. self.assertEqual(
  603. self.serialize(
  604. MockEvent(
  605. sender="@alice:localhost",
  606. room_id="!foo:bar",
  607. content={
  608. "not_me": 1,
  609. "nested.dot.key": {"leaf.key": 42, "not_me_either": 1},
  610. },
  611. ),
  612. [r"content.nested\.dot\.key.leaf\.key"],
  613. ),
  614. {"content": {"nested.dot.key": {"leaf.key": 42}}},
  615. )
  616. def test_event_fields_nops_with_unknown_keys(self) -> None:
  617. self.assertEqual(
  618. self.serialize(
  619. MockEvent(
  620. sender="@alice:localhost",
  621. room_id="!foo:bar",
  622. content={"foo": "bar"},
  623. ),
  624. ["content.foo", "content.notexists"],
  625. ),
  626. {"content": {"foo": "bar"}},
  627. )
  628. def test_event_fields_nops_with_non_dict_keys(self) -> None:
  629. self.assertEqual(
  630. self.serialize(
  631. MockEvent(
  632. sender="@alice:localhost",
  633. room_id="!foo:bar",
  634. content={"foo": ["I", "am", "an", "array"]},
  635. ),
  636. ["content.foo.am"],
  637. ),
  638. {},
  639. )
  640. def test_event_fields_nops_with_array_keys(self) -> None:
  641. self.assertEqual(
  642. self.serialize(
  643. MockEvent(
  644. sender="@alice:localhost",
  645. room_id="!foo:bar",
  646. content={"foo": ["I", "am", "an", "array"]},
  647. ),
  648. ["content.foo.1"],
  649. ),
  650. {},
  651. )
  652. def test_event_fields_all_fields_if_empty(self) -> None:
  653. self.assertEqual(
  654. self.serialize(
  655. MockEvent(
  656. type="foo",
  657. event_id="test",
  658. room_id="!foo:bar",
  659. content={"foo": "bar"},
  660. ),
  661. [],
  662. ),
  663. {
  664. "type": "foo",
  665. "event_id": "test",
  666. "room_id": "!foo:bar",
  667. "content": {"foo": "bar"},
  668. "unsigned": {},
  669. },
  670. )
  671. def test_event_fields_fail_if_fields_not_str(self) -> None:
  672. with self.assertRaises(TypeError):
  673. self.serialize(
  674. MockEvent(room_id="!foo:bar", content={"foo": "bar"}), ["room_id", 4] # type: ignore[list-item]
  675. )
  676. class CopyPowerLevelsContentTestCase(stdlib_unittest.TestCase):
  677. def setUp(self) -> None:
  678. self.test_content: PowerLevelsContent = {
  679. "ban": 50,
  680. "events": {"m.room.name": 100, "m.room.power_levels": 100},
  681. "events_default": 0,
  682. "invite": 50,
  683. "kick": 50,
  684. "notifications": {"room": 20},
  685. "redact": 50,
  686. "state_default": 50,
  687. "users": {"@example:localhost": 100},
  688. "users_default": 0,
  689. }
  690. def _test(self, input: PowerLevelsContent) -> None:
  691. a = copy_and_fixup_power_levels_contents(input)
  692. self.assertEqual(a["ban"], 50)
  693. assert isinstance(a["events"], Mapping)
  694. self.assertEqual(a["events"]["m.room.name"], 100)
  695. # make sure that changing the copy changes the copy and not the orig
  696. a["ban"] = 10
  697. a["events"]["m.room.power_levels"] = 20
  698. self.assertEqual(input["ban"], 50)
  699. assert isinstance(input["events"], Mapping)
  700. self.assertEqual(input["events"]["m.room.power_levels"], 100)
  701. def test_unfrozen(self) -> None:
  702. self._test(self.test_content)
  703. def test_frozen(self) -> None:
  704. input = freeze(self.test_content)
  705. self._test(input)
  706. def test_stringy_integers(self) -> None:
  707. """String representations of decimal integers are converted to integers."""
  708. input: PowerLevelsContent = {
  709. "a": "100",
  710. "b": {
  711. "foo": 99,
  712. "bar": "-98",
  713. },
  714. "d": "0999",
  715. }
  716. output = copy_and_fixup_power_levels_contents(input)
  717. expected_output = {
  718. "a": 100,
  719. "b": {
  720. "foo": 99,
  721. "bar": -98,
  722. },
  723. "d": 999,
  724. }
  725. self.assertEqual(output, expected_output)
  726. def test_strings_that_dont_represent_decimal_integers(self) -> None:
  727. """Should raise when given inputs `s` for which `int(s, base=10)` raises."""
  728. for invalid_string in ["0x123", "123.0", "123.45", "hello", "0b1", "0o777"]:
  729. with self.assertRaises(TypeError):
  730. copy_and_fixup_power_levels_contents({"a": invalid_string})
  731. def test_invalid_types_raise_type_error(self) -> None:
  732. with self.assertRaises(TypeError):
  733. copy_and_fixup_power_levels_contents({"a": ["hello", "grandma"]}) # type: ignore[dict-item]
  734. copy_and_fixup_power_levels_contents({"a": None}) # type: ignore[dict-item]
  735. def test_invalid_nesting_raises_type_error(self) -> None:
  736. with self.assertRaises(TypeError):
  737. copy_and_fixup_power_levels_contents({"a": {"b": {"c": 1}}}) # type: ignore[dict-item]
  738. class SplitFieldTestCase(stdlib_unittest.TestCase):
  739. @parameterized.expand(
  740. [
  741. # A field with no dots.
  742. ["m", ["m"]],
  743. # Simple dotted fields.
  744. ["m.foo", ["m", "foo"]],
  745. ["m.foo.bar", ["m", "foo", "bar"]],
  746. # Backslash is used as an escape character.
  747. [r"m\.foo", ["m.foo"]],
  748. [r"m\\.foo", ["m\\", "foo"]],
  749. [r"m\\\.foo", [r"m\.foo"]],
  750. [r"m\\\\.foo", ["m\\\\", "foo"]],
  751. [r"m\foo", [r"m\foo"]],
  752. [r"m\\foo", [r"m\foo"]],
  753. [r"m\\\foo", [r"m\\foo"]],
  754. [r"m\\\\foo", [r"m\\foo"]],
  755. # Ensure that escapes at the end don't cause issues.
  756. ["m.foo\\", ["m", "foo\\"]],
  757. ["m.foo\\", ["m", "foo\\"]],
  758. [r"m.foo\.", ["m", "foo."]],
  759. [r"m.foo\\.", ["m", "foo\\", ""]],
  760. [r"m.foo\\\.", ["m", r"foo\."]],
  761. # Empty parts (corresponding to properties which are an empty string) are allowed.
  762. [".m", ["", "m"]],
  763. ["..m", ["", "", "m"]],
  764. ["m.", ["m", ""]],
  765. ["m..", ["m", "", ""]],
  766. ["m..foo", ["m", "", "foo"]],
  767. # Invalid escape sequences.
  768. [r"\m", [r"\m"]],
  769. ]
  770. )
  771. def test_split_field(self, input: str, expected: str) -> None:
  772. self.assertEqual(_split_field(input), expected)