|
| 1 | +# Copyright 2021 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 | + |
| 15 | +from typing import Iterable, Optional, Tuple |
| 16 | + |
| 17 | +from synapse.api.constants import EventTypes, Membership |
| 18 | +from synapse.api.room_versions import RoomVersions |
| 19 | +from synapse.events import FrozenEvent |
| 20 | +from synapse.push.presentable_names import calculate_room_name |
| 21 | +from synapse.types import StateKey, StateMap |
| 22 | + |
| 23 | +from tests import unittest |
| 24 | + |
| 25 | + |
| 26 | +class MockDataStore: |
| 27 | + """ |
| 28 | + A fake data store which stores a mapping of state key to event content. |
| 29 | + (I.e. the state key is used as the event ID.) |
| 30 | + """ |
| 31 | + |
| 32 | + def __init__(self, events: Iterable[Tuple[StateKey, dict]]): |
| 33 | + """ |
| 34 | + Args: |
| 35 | + events: A state map to event contents. |
| 36 | + """ |
| 37 | + self._events = {} |
| 38 | + |
| 39 | + for i, (event_id, content) in enumerate(events): |
| 40 | + self._events[event_id] = FrozenEvent( |
| 41 | + { |
| 42 | + "event_id": "$event_id", |
| 43 | + "type": event_id[0], |
| 44 | + "sender": "@user:test", |
| 45 | + "state_key": event_id[1], |
| 46 | + "room_id": "#room:test", |
| 47 | + "content": content, |
| 48 | + "origin_server_ts": i, |
| 49 | + }, |
| 50 | + RoomVersions.V1, |
| 51 | + ) |
| 52 | + |
| 53 | + async def get_event( |
| 54 | + self, event_id: StateKey, allow_none: bool = False |
| 55 | + ) -> Optional[FrozenEvent]: |
| 56 | + assert allow_none, "Mock not configured for allow_none = False" |
| 57 | + |
| 58 | + return self._events.get(event_id) |
| 59 | + |
| 60 | + async def get_events(self, event_ids: Iterable[StateKey]): |
| 61 | + # This is cheating since it just returns all events. |
| 62 | + return self._events |
| 63 | + |
| 64 | + |
| 65 | +class PresentableNamesTestCase(unittest.HomeserverTestCase): |
| 66 | + USER_ID = "@test:test" |
| 67 | + OTHER_USER_ID = "@user:test" |
| 68 | + |
| 69 | + def _calculate_room_name( |
| 70 | + self, |
| 71 | + events: StateMap[dict], |
| 72 | + user_id: str = "", |
| 73 | + fallback_to_members: bool = True, |
| 74 | + fallback_to_single_member: bool = True, |
| 75 | + ): |
| 76 | + # This isn't 100% accurate, but works with MockDataStore. |
| 77 | + room_state_ids = {k[0]: k[0] for k in events} |
| 78 | + |
| 79 | + return self.get_success( |
| 80 | + calculate_room_name( |
| 81 | + MockDataStore(events), |
| 82 | + room_state_ids, |
| 83 | + user_id or self.USER_ID, |
| 84 | + fallback_to_members, |
| 85 | + fallback_to_single_member, |
| 86 | + ) |
| 87 | + ) |
| 88 | + |
| 89 | + def test_name(self): |
| 90 | + """A room name event should be used.""" |
| 91 | + events = [ |
| 92 | + ((EventTypes.Name, ""), {"name": "test-name"}), |
| 93 | + ] |
| 94 | + self.assertEqual("test-name", self._calculate_room_name(events)) |
| 95 | + |
| 96 | + # Check if the event content has garbage. |
| 97 | + events = [((EventTypes.Name, ""), {"foo": 1})] |
| 98 | + self.assertEqual("Empty Room", self._calculate_room_name(events)) |
| 99 | + |
| 100 | + events = [((EventTypes.Name, ""), {"name": 1})] |
| 101 | + self.assertEqual(1, self._calculate_room_name(events)) |
| 102 | + |
| 103 | + def test_canonical_alias(self): |
| 104 | + """An canonical alias should be used.""" |
| 105 | + events = [ |
| 106 | + ((EventTypes.CanonicalAlias, ""), {"alias": "#test-name:test"}), |
| 107 | + ] |
| 108 | + self.assertEqual("#test-name:test", self._calculate_room_name(events)) |
| 109 | + |
| 110 | + # Check if the event content has garbage. |
| 111 | + events = [((EventTypes.CanonicalAlias, ""), {"foo": 1})] |
| 112 | + self.assertEqual("Empty Room", self._calculate_room_name(events)) |
| 113 | + |
| 114 | + events = [((EventTypes.CanonicalAlias, ""), {"alias": "test-name"})] |
| 115 | + self.assertEqual("Empty Room", self._calculate_room_name(events)) |
| 116 | + |
| 117 | + def test_invite(self): |
| 118 | + """An invite has special behaviour.""" |
| 119 | + events = [ |
| 120 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), |
| 121 | + ((EventTypes.Member, self.OTHER_USER_ID), {"displayname": "Other User"}), |
| 122 | + ] |
| 123 | + self.assertEqual("Invite from Other User", self._calculate_room_name(events)) |
| 124 | + self.assertIsNone( |
| 125 | + self._calculate_room_name(events, fallback_to_single_member=False) |
| 126 | + ) |
| 127 | + # Ensure this logic is skipped if we don't fallback to members. |
| 128 | + self.assertIsNone(self._calculate_room_name(events, fallback_to_members=False)) |
| 129 | + |
| 130 | + # Check if the event content has garbage. |
| 131 | + events = [ |
| 132 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), |
| 133 | + ((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}), |
| 134 | + ] |
| 135 | + self.assertEqual("Invite from @user:test", self._calculate_room_name(events)) |
| 136 | + |
| 137 | + # No member event for sender. |
| 138 | + events = [ |
| 139 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}), |
| 140 | + ] |
| 141 | + self.assertEqual("Room Invite", self._calculate_room_name(events)) |
| 142 | + |
| 143 | + def test_no_members(self): |
| 144 | + """Behaviour of an empty room.""" |
| 145 | + events = [] |
| 146 | + self.assertEqual("Empty Room", self._calculate_room_name(events)) |
| 147 | + |
| 148 | + # Note that events with invalid (or missing) membership are ignored. |
| 149 | + events = [ |
| 150 | + ((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}), |
| 151 | + ((EventTypes.Member, "@foo:test"), {"membership": "foo"}), |
| 152 | + ] |
| 153 | + self.assertEqual("Empty Room", self._calculate_room_name(events)) |
| 154 | + |
| 155 | + def test_no_other_members(self): |
| 156 | + """Behaviour of a room with no other members in it.""" |
| 157 | + events = [ |
| 158 | + ( |
| 159 | + (EventTypes.Member, self.USER_ID), |
| 160 | + {"membership": Membership.JOIN, "displayname": "Me"}, |
| 161 | + ), |
| 162 | + ] |
| 163 | + self.assertEqual("Me", self._calculate_room_name(events)) |
| 164 | + |
| 165 | + # Check if the event content has no displayname. |
| 166 | + events = [ |
| 167 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), |
| 168 | + ] |
| 169 | + self.assertEqual("@test:test", self._calculate_room_name(events)) |
| 170 | + |
| 171 | + # 3pid invite, use the other user (who is set as the sender). |
| 172 | + events = [ |
| 173 | + ((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}), |
| 174 | + ] |
| 175 | + self.assertEqual( |
| 176 | + "nobody", self._calculate_room_name(events, user_id=self.OTHER_USER_ID) |
| 177 | + ) |
| 178 | + |
| 179 | + events = [ |
| 180 | + ((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}), |
| 181 | + ((EventTypes.ThirdPartyInvite, self.OTHER_USER_ID), {}), |
| 182 | + ] |
| 183 | + self.assertEqual( |
| 184 | + "Inviting email address", |
| 185 | + self._calculate_room_name(events, user_id=self.OTHER_USER_ID), |
| 186 | + ) |
| 187 | + |
| 188 | + def test_one_other_member(self): |
| 189 | + """Behaviour of a room with a single other member.""" |
| 190 | + events = [ |
| 191 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), |
| 192 | + ( |
| 193 | + (EventTypes.Member, self.OTHER_USER_ID), |
| 194 | + {"membership": Membership.JOIN, "displayname": "Other User"}, |
| 195 | + ), |
| 196 | + ] |
| 197 | + self.assertEqual("Other User", self._calculate_room_name(events)) |
| 198 | + self.assertIsNone( |
| 199 | + self._calculate_room_name(events, fallback_to_single_member=False) |
| 200 | + ) |
| 201 | + |
| 202 | + # Check if the event content has no displayname and is an invite. |
| 203 | + events = [ |
| 204 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), |
| 205 | + ( |
| 206 | + (EventTypes.Member, self.OTHER_USER_ID), |
| 207 | + {"membership": Membership.INVITE}, |
| 208 | + ), |
| 209 | + ] |
| 210 | + self.assertEqual("@user:test", self._calculate_room_name(events)) |
| 211 | + |
| 212 | + def test_other_members(self): |
| 213 | + """Behaviour of a room with multiple other members.""" |
| 214 | + # Two other members. |
| 215 | + events = [ |
| 216 | + ((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}), |
| 217 | + ( |
| 218 | + (EventTypes.Member, self.OTHER_USER_ID), |
| 219 | + {"membership": Membership.JOIN, "displayname": "Other User"}, |
| 220 | + ), |
| 221 | + ((EventTypes.Member, "@foo:test"), {"membership": Membership.JOIN}), |
| 222 | + ] |
| 223 | + self.assertEqual("Other User and @foo:test", self._calculate_room_name(events)) |
| 224 | + |
| 225 | + # Three or more other members. |
| 226 | + events.append( |
| 227 | + ((EventTypes.Member, "@fourth:test"), {"membership": Membership.INVITE}) |
| 228 | + ) |
| 229 | + self.assertEqual("Other User and 2 others", self._calculate_room_name(events)) |
0 commit comments