From 29941ddb08c3cbac77dae7f129dc22cd41c097cf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 19 Jan 2021 14:45:05 -0500 Subject: [PATCH 1/7] Avoid trusting the content when calculating presentable names. --- changelog.d/9165.bugfix | 1 + synapse/push/presentable_names.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 changelog.d/9165.bugfix diff --git a/changelog.d/9165.bugfix b/changelog.d/9165.bugfix new file mode 100644 index 000000000000..8a52dbe566b5 --- /dev/null +++ b/changelog.d/9165.bugfix @@ -0,0 +1 @@ +Fix a longstanding bug where invalid data could cause errors when calculating the presentable room name for push. diff --git a/synapse/push/presentable_names.py b/synapse/push/presentable_names.py index 7e50341d7479..2c3c3a2f0bcb 100644 --- a/synapse/push/presentable_names.py +++ b/synapse/push/presentable_names.py @@ -17,7 +17,7 @@ import re from typing import TYPE_CHECKING, Dict, Iterable, Optional -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership from synapse.events import EventBase from synapse.types import StateMap @@ -63,7 +63,7 @@ async def calculate_room_name( m_room_name = await store.get_event( room_state_ids[(EventTypes.Name, "")], allow_none=True ) - if m_room_name and m_room_name.content and m_room_name.content["name"]: + if m_room_name and m_room_name.content and m_room_name.content.get("name"): return m_room_name.content["name"] # does it have a canonical alias? @@ -74,7 +74,7 @@ async def calculate_room_name( if ( canon_alias and canon_alias.content - and canon_alias.content["alias"] + and canon_alias.content.get("alias") and _looks_like_an_alias(canon_alias.content["alias"]) ): return canon_alias.content["alias"] @@ -94,7 +94,7 @@ async def calculate_room_name( if ( my_member_event is not None - and my_member_event.content["membership"] == "invite" + and my_member_event.content.get("membership") == Membership.INVITE ): if (EventTypes.Member, my_member_event.sender) in room_state_ids: inviter_member_event = await store.get_event( @@ -120,8 +120,8 @@ async def calculate_room_name( all_members = [ ev for ev in member_events.values() - if ev.content["membership"] == "join" - or ev.content["membership"] == "invite" + if ev.content.get("membership") == Membership.JOIN + or ev.content.get("membership") == Membership.INVITE ] # Sort the member events oldest-first so the we name people in the # order the joined (it should at least be deterministic rather than @@ -194,11 +194,7 @@ def descriptor_from_member_events(member_events: Iterable[EventBase]) -> str: def name_from_member_event(member_event: EventBase) -> str: - if ( - member_event.content - and "displayname" in member_event.content - and member_event.content["displayname"] - ): + if member_event.content and member_event.content.get("displayname"): return member_event.content["displayname"] return member_event.state_key From 158687806a63b2570931d5cbf30f7ea62fafb5af Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 20 Jan 2021 14:16:05 -0500 Subject: [PATCH 2/7] Add tests for presentable names. --- tests/push/test_presentable_names.py | 231 +++++++++++++++++++++++++ tests/push/test_push_rule_evaluator.py | 2 +- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 tests/push/test_presentable_names.py diff --git a/tests/push/test_presentable_names.py b/tests/push/test_presentable_names.py new file mode 100644 index 000000000000..ae1da6d464a0 --- /dev/null +++ b/tests/push/test_presentable_names.py @@ -0,0 +1,231 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterable, Optional + +from synapse.api.constants import EventTypes, Membership +from synapse.api.room_versions import RoomVersions +from synapse.events import FrozenEvent +from synapse.push.presentable_names import calculate_room_name +from synapse.types import StateKey, StateMap + +from tests import unittest + + +class MockDataStore: + """ + A fake data store which stores a mapping of state key to event content. + (I.e. the state key is used as the event ID.) + """ + + def __init__(self, events: StateMap[dict]): + """ + Params: + events: A state map to event contents. + """ + self._events = events + + async def get_event( + self, event_id: StateKey, allow_none: bool = False + ) -> Optional[FrozenEvent]: + assert allow_none, "Mock not configured for allow_none = False" + + content = self._events.get(event_id) + if not content: + return None + + return FrozenEvent( + { + "event_id": "$event_id", + "type": event_id[0], + "sender": "@user:test", + "state_key": event_id[1], + "room_id": "#room:test", + "content": content, + "origin_server_ts": 1, + }, + RoomVersions.V1, + ) + + async def get_events(self, event_ids: Iterable[StateKey]): + results = {} + for event_id in event_ids: + event = await self.get_event(event_id, allow_none=True) + if event: + results[event_id] = event + return results + + +class PresentableNamesTestCase(unittest.HomeserverTestCase): + USER_ID = "@test:test" + OTHER_USER_ID = "@user:test" + + def _calculate_room_name( + self, + events: StateMap[dict], + user_id: str = "", + fallback_to_members: bool = True, + fallback_to_single_member: bool = True, + ): + # This isn't 100% accurate, but works with MockDataStore. + room_state_ids = {k: k for k in events} + + return self.get_success( + calculate_room_name( + MockDataStore(events), + room_state_ids, + user_id or self.USER_ID, + fallback_to_members, + fallback_to_single_member, + ) + ) + + def test_name(self): + """A room name event should be used.""" + events = { + (EventTypes.Name, ""): {"name": "test-name"}, + } + self.assertEqual("test-name", self._calculate_room_name(events)) + + # Check if the event content has garbage. + events = {(EventTypes.Name, ""): {"foo": 1}} + self.assertEqual("Empty Room", self._calculate_room_name(events)) + + events = {(EventTypes.Name, ""): {"name": 1}} + self.assertEqual(1, self._calculate_room_name(events)) + + def test_canonical_alias(self): + """An canonical alias should be used.""" + events = { + (EventTypes.CanonicalAlias, ""): {"alias": "#test-name:test"}, + } + self.assertEqual("#test-name:test", self._calculate_room_name(events)) + + # Check if the event content has garbage. + events = {(EventTypes.CanonicalAlias, ""): {"foo": 1}} + self.assertEqual("Empty Room", self._calculate_room_name(events)) + + events = {(EventTypes.CanonicalAlias, ""): {"alias": "test-name"}} + self.assertEqual("Empty Room", self._calculate_room_name(events)) + + def test_invite(self): + """An invite has special behaviour.""" + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, + (EventTypes.Member, self.OTHER_USER_ID): {"displayname": "Other User"}, + } + self.assertEqual("Invite from Other User", self._calculate_room_name(events)) + self.assertIsNone( + self._calculate_room_name(events, fallback_to_single_member=False) + ) + # Ensure this logic is skipped if we don't fallback to members. + self.assertIsNone(self._calculate_room_name(events, fallback_to_members=False)) + + # Check if the event content has garbage. + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, + (EventTypes.Member, self.OTHER_USER_ID): {"foo": 1}, + } + self.assertEqual("Invite from @user:test", self._calculate_room_name(events)) + + # No member event for sender. + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, + } + self.assertEqual("Room Invite", self._calculate_room_name(events)) + + def test_no_members(self): + """Behaviour of an empty room.""" + events = {} + self.assertEqual("Empty Room", self._calculate_room_name(events)) + + # Note that events with invalid (or missing) membership are ignored. + events = { + (EventTypes.Member, self.USER_ID): {}, + (EventTypes.Member, self.OTHER_USER_ID): {"membership": "foo"}, + } + self.assertEqual("Empty Room", self._calculate_room_name(events)) + + def test_no_other_members(self): + """Behaviour of a room with no other members in it.""" + events = { + (EventTypes.Member, self.USER_ID): { + "membership": Membership.JOIN, + "displayname": "Me", + }, + } + self.assertEqual("Me", self._calculate_room_name(events)) + + # Check if the event content has no displayname. + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, + } + self.assertEqual("@test:test", self._calculate_room_name(events)) + + # 3pid invite, use the other user (who is set as the sender). + events = { + (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.JOIN}, + } + self.assertEqual( + "nobody", self._calculate_room_name(events, user_id=self.OTHER_USER_ID) + ) + + events = { + (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.JOIN}, + (EventTypes.ThirdPartyInvite, self.OTHER_USER_ID): {}, + } + self.assertEqual( + "Inviting email address", + self._calculate_room_name(events, user_id=self.OTHER_USER_ID), + ) + + def test_one_other_member(self): + """Behaviour of a room with a single other member.""" + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, + (EventTypes.Member, self.OTHER_USER_ID): { + "membership": Membership.JOIN, + "displayname": "Other User", + }, + } + self.assertEqual("Other User", self._calculate_room_name(events)) + self.assertIsNone( + self._calculate_room_name(events, fallback_to_single_member=False) + ) + + # Check if the event content has no displayname and is an invite. + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, + (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.INVITE}, + } + self.assertEqual("@user:test", self._calculate_room_name(events)) + + def test_other_members(self): + """Behaviour of a room with multiple other members.""" + # Two other members. + events = { + (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, + (EventTypes.Member, self.OTHER_USER_ID): { + "membership": Membership.JOIN, + "displayname": "Other User", + }, + (EventTypes.Member, "@foo:test"): {"membership": Membership.JOIN}, + } + self.assertEqual("Other User and @foo:test", self._calculate_room_name(events)) + + # Three or more other members. + events[(EventTypes.Member, "@fourth:test")] = { + "membership": Membership.INVITE, + } + self.assertEqual("Other User and 2 others", self._calculate_room_name(events)) diff --git a/tests/push/test_push_rule_evaluator.py b/tests/push/test_push_rule_evaluator.py index 1f4b5ca2ac11..4a841f5bb844 100644 --- a/tests/push/test_push_rule_evaluator.py +++ b/tests/push/test_push_rule_evaluator.py @@ -29,7 +29,7 @@ def _get_evaluator(self, content): "type": "m.room.history_visibility", "sender": "@user:test", "state_key": "", - "room_id": "@room:test", + "room_id": "#room:test", "content": content, }, RoomVersions.V1, From c40d61b99fa3bdd1991ca542ced7125b3382a8a7 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 20 Jan 2021 14:18:06 -0500 Subject: [PATCH 3/7] Slight optimization -- avoid calculating something until it is needed. --- synapse/push/presentable_names.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/push/presentable_names.py b/synapse/push/presentable_names.py index 2c3c3a2f0bcb..04c2c1482ce2 100644 --- a/synapse/push/presentable_names.py +++ b/synapse/push/presentable_names.py @@ -79,10 +79,6 @@ async def calculate_room_name( ): return canon_alias.content["alias"] - # at this point we're going to need to search the state by all state keys - # for an event type, so rearrange the data structure - room_state_bytype_ids = _state_as_two_level_dict(room_state_ids) - if not fallback_to_members: return None @@ -111,6 +107,10 @@ async def calculate_room_name( else: return "Room Invite" + # at this point we're going to need to search the state by all state keys + # for an event type, so rearrange the data structure + room_state_bytype_ids = _state_as_two_level_dict(room_state_ids) + # we're going to have to generate a name based on who's in the room, # so find out who is in the room that isn't the user. if EventTypes.Member in room_state_bytype_ids: From 381b3d815e79a738af1c059f62aa22387cd76ccb Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 20 Jan 2021 14:30:47 -0500 Subject: [PATCH 4/7] Slight tweak to tests. --- tests/push/test_presentable_names.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/push/test_presentable_names.py b/tests/push/test_presentable_names.py index ae1da6d464a0..b21403cbce66 100644 --- a/tests/push/test_presentable_names.py +++ b/tests/push/test_presentable_names.py @@ -152,8 +152,8 @@ def test_no_members(self): # Note that events with invalid (or missing) membership are ignored. events = { - (EventTypes.Member, self.USER_ID): {}, - (EventTypes.Member, self.OTHER_USER_ID): {"membership": "foo"}, + (EventTypes.Member, self.OTHER_USER_ID): {"foo": 1}, + (EventTypes.Member, "@foo:test"): {"membership": "foo"}, } self.assertEqual("Empty Room", self._calculate_room_name(events)) From 7b3fd05a72e9ba5525810ac8dd4cda1d0fe7bf97 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 20 Jan 2021 14:41:20 -0500 Subject: [PATCH 5/7] Typo --- changelog.d/9165.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/9165.bugfix b/changelog.d/9165.bugfix index 8a52dbe566b5..58db22f484b0 100644 --- a/changelog.d/9165.bugfix +++ b/changelog.d/9165.bugfix @@ -1 +1 @@ -Fix a longstanding bug where invalid data could cause errors when calculating the presentable room name for push. +Fix a long-standing bug where invalid data could cause errors when calculating the presentable room name for push. From 7acb3b428d2ddd0e004e23b90f77081d60ac9c36 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 20 Jan 2021 15:32:26 -0500 Subject: [PATCH 6/7] Fix tests for Py3.5. --- tests/push/test_presentable_names.py | 178 +++++++++++++-------------- 1 file changed, 88 insertions(+), 90 deletions(-) diff --git a/tests/push/test_presentable_names.py b/tests/push/test_presentable_names.py index b21403cbce66..0c14274cc88a 100644 --- a/tests/push/test_presentable_names.py +++ b/tests/push/test_presentable_names.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Iterable, Optional +from typing import Iterable, Optional, Tuple from synapse.api.constants import EventTypes, Membership from synapse.api.room_versions import RoomVersions @@ -29,42 +29,37 @@ class MockDataStore: (I.e. the state key is used as the event ID.) """ - def __init__(self, events: StateMap[dict]): + def __init__(self, events: Iterable[Tuple[StateKey, dict]]): """ Params: events: A state map to event contents. """ - self._events = events + self._events = {} + + for i, (event_id, content) in enumerate(events): + self._events[event_id] = FrozenEvent( + { + "event_id": "$event_id", + "type": event_id[0], + "sender": "@user:test", + "state_key": event_id[1], + "room_id": "#room:test", + "content": content, + "origin_server_ts": i, + }, + RoomVersions.V1, + ) async def get_event( self, event_id: StateKey, allow_none: bool = False ) -> Optional[FrozenEvent]: assert allow_none, "Mock not configured for allow_none = False" - content = self._events.get(event_id) - if not content: - return None - - return FrozenEvent( - { - "event_id": "$event_id", - "type": event_id[0], - "sender": "@user:test", - "state_key": event_id[1], - "room_id": "#room:test", - "content": content, - "origin_server_ts": 1, - }, - RoomVersions.V1, - ) + return self._events.get(event_id) async def get_events(self, event_ids: Iterable[StateKey]): - results = {} - for event_id in event_ids: - event = await self.get_event(event_id, allow_none=True) - if event: - results[event_id] = event - return results + # This is cheating since it just returns all events. + return self._events class PresentableNamesTestCase(unittest.HomeserverTestCase): @@ -79,7 +74,7 @@ def _calculate_room_name( fallback_to_single_member: bool = True, ): # This isn't 100% accurate, but works with MockDataStore. - room_state_ids = {k: k for k in events} + room_state_ids = {k[0]: k[0] for k in events} return self.get_success( calculate_room_name( @@ -93,38 +88,38 @@ def _calculate_room_name( def test_name(self): """A room name event should be used.""" - events = { - (EventTypes.Name, ""): {"name": "test-name"}, - } + events = [ + ((EventTypes.Name, ""), {"name": "test-name"}), + ] self.assertEqual("test-name", self._calculate_room_name(events)) # Check if the event content has garbage. - events = {(EventTypes.Name, ""): {"foo": 1}} + events = [((EventTypes.Name, ""), {"foo": 1})] self.assertEqual("Empty Room", self._calculate_room_name(events)) - events = {(EventTypes.Name, ""): {"name": 1}} + events = [((EventTypes.Name, ""), {"name": 1})] self.assertEqual(1, self._calculate_room_name(events)) def test_canonical_alias(self): """An canonical alias should be used.""" - events = { - (EventTypes.CanonicalAlias, ""): {"alias": "#test-name:test"}, - } + events = [ + ((EventTypes.CanonicalAlias, ""), {"alias": "#test-name:test"}), + ] self.assertEqual("#test-name:test", self._calculate_room_name(events)) # Check if the event content has garbage. - events = {(EventTypes.CanonicalAlias, ""): {"foo": 1}} + events = [((EventTypes.CanonicalAlias, ""), {"foo": 1})] self.assertEqual("Empty Room", self._calculate_room_name(events)) - events = {(EventTypes.CanonicalAlias, ""): {"alias": "test-name"}} + events = [((EventTypes.CanonicalAlias, ""), {"alias": "test-name"})] self.assertEqual("Empty Room", self._calculate_room_name(events)) def test_invite(self): """An invite has special behaviour.""" - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, - (EventTypes.Member, self.OTHER_USER_ID): {"displayname": "Other User"}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), + ((EventTypes.Member, self.OTHER_USER_ID), {"displayname": "Other User"}), + ] self.assertEqual("Invite from Other User", self._calculate_room_name(events)) self.assertIsNone( self._calculate_room_name(events, fallback_to_single_member=False) @@ -133,58 +128,58 @@ def test_invite(self): self.assertIsNone(self._calculate_room_name(events, fallback_to_members=False)) # Check if the event content has garbage. - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, - (EventTypes.Member, self.OTHER_USER_ID): {"foo": 1}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), + ((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}), + ] self.assertEqual("Invite from @user:test", self._calculate_room_name(events)) # No member event for sender. - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.INVITE}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), + ] self.assertEqual("Room Invite", self._calculate_room_name(events)) def test_no_members(self): """Behaviour of an empty room.""" - events = {} + events = [] self.assertEqual("Empty Room", self._calculate_room_name(events)) # Note that events with invalid (or missing) membership are ignored. - events = { - (EventTypes.Member, self.OTHER_USER_ID): {"foo": 1}, - (EventTypes.Member, "@foo:test"): {"membership": "foo"}, - } + events = [ + ((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}), + ((EventTypes.Member, "@foo:test"), {"membership": "foo"}), + ] self.assertEqual("Empty Room", self._calculate_room_name(events)) def test_no_other_members(self): """Behaviour of a room with no other members in it.""" - events = { - (EventTypes.Member, self.USER_ID): { - "membership": Membership.JOIN, - "displayname": "Me", - }, - } + events = [ + ( + (EventTypes.Member, self.USER_ID), + {"membership": Membership.JOIN, "displayname": "Me"}, + ), + ] self.assertEqual("Me", self._calculate_room_name(events)) # Check if the event content has no displayname. - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), + ] self.assertEqual("@test:test", self._calculate_room_name(events)) # 3pid invite, use the other user (who is set as the sender). - events = { - (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.JOIN}, - } + events = [ + ((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}), + ] self.assertEqual( "nobody", self._calculate_room_name(events, user_id=self.OTHER_USER_ID) ) - events = { - (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.JOIN}, - (EventTypes.ThirdPartyInvite, self.OTHER_USER_ID): {}, - } + events = [ + ((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}), + ((EventTypes.ThirdPartyInvite, self.OTHER_USER_ID), {}), + ] self.assertEqual( "Inviting email address", self._calculate_room_name(events, user_id=self.OTHER_USER_ID), @@ -192,40 +187,43 @@ def test_no_other_members(self): def test_one_other_member(self): """Behaviour of a room with a single other member.""" - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, - (EventTypes.Member, self.OTHER_USER_ID): { - "membership": Membership.JOIN, - "displayname": "Other User", - }, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), + ( + (EventTypes.Member, self.OTHER_USER_ID), + {"membership": Membership.JOIN, "displayname": "Other User"}, + ), + ] self.assertEqual("Other User", self._calculate_room_name(events)) self.assertIsNone( self._calculate_room_name(events, fallback_to_single_member=False) ) # Check if the event content has no displayname and is an invite. - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, - (EventTypes.Member, self.OTHER_USER_ID): {"membership": Membership.INVITE}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), + ( + (EventTypes.Member, self.OTHER_USER_ID), + {"membership": Membership.INVITE}, + ), + ] self.assertEqual("@user:test", self._calculate_room_name(events)) def test_other_members(self): """Behaviour of a room with multiple other members.""" # Two other members. - events = { - (EventTypes.Member, self.USER_ID): {"membership": Membership.JOIN}, - (EventTypes.Member, self.OTHER_USER_ID): { - "membership": Membership.JOIN, - "displayname": "Other User", - }, - (EventTypes.Member, "@foo:test"): {"membership": Membership.JOIN}, - } + events = [ + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), + ( + (EventTypes.Member, self.OTHER_USER_ID), + {"membership": Membership.JOIN, "displayname": "Other User"}, + ), + ((EventTypes.Member, "@foo:test"), {"membership": Membership.JOIN}), + ] self.assertEqual("Other User and @foo:test", self._calculate_room_name(events)) # Three or more other members. - events[(EventTypes.Member, "@fourth:test")] = { - "membership": Membership.INVITE, - } + events.append( + ((EventTypes.Member, "@fourth:test"), {"membership": Membership.INVITE}) + ) self.assertEqual("Other User and 2 others", self._calculate_room_name(events)) From b2c420bd06762d2e53eb99e33401fde20425bde3 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 21 Jan 2021 13:56:13 -0500 Subject: [PATCH 7/7] Params -> Args --- tests/push/test_presentable_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/push/test_presentable_names.py b/tests/push/test_presentable_names.py index 0c14274cc88a..aff563919d43 100644 --- a/tests/push/test_presentable_names.py +++ b/tests/push/test_presentable_names.py @@ -31,7 +31,7 @@ class MockDataStore: def __init__(self, events: Iterable[Tuple[StateKey, dict]]): """ - Params: + Args: events: A state map to event contents. """ self._events = {}